From 6cff72134f843f97d4f6af1e57ec9bba30d674e6 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Tue, 3 Sep 2024 15:35:48 +0200 Subject: [PATCH 01/31] Added a binary time decoder for parameters This allows easier (than in pure XTCE) decoding of parameter times and using leap seconds as well as time correlation service. --- .../org/yamcs/algo/TimeBinaryDecoder.java | 87 +++++++++++++++++++ .../org/yamcs/archive/XtceTmRecorder.java | 7 +- .../org/yamcs/mdb/DataDecoderFactory.java | 5 +- .../org/yamcs/mdb/DataEncoderFactory.java | 19 ++-- .../org/yamcs/mdb/ParameterTypeProcessor.java | 14 ++- .../java/org/yamcs/mdb/ProcessorData.java | 18 ++-- .../java/org/yamcs/mdb/XtceTmProcessor.java | 4 +- .../yamcs/time/TimeCorrelationService.java | 36 +++----- .../java/org/yamcs/xtce/DataEncoding.java | 30 +++++-- .../org/yamcs/xtce/xml/XtceStaxReader.java | 13 ++- 10 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 yamcs-core/src/main/java/org/yamcs/algo/TimeBinaryDecoder.java diff --git a/yamcs-core/src/main/java/org/yamcs/algo/TimeBinaryDecoder.java b/yamcs-core/src/main/java/org/yamcs/algo/TimeBinaryDecoder.java new file mode 100644 index 00000000000..6a5bea46702 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/algo/TimeBinaryDecoder.java @@ -0,0 +1,87 @@ +package org.yamcs.algo; + +import java.util.Map; + +import org.yamcs.ConfigurationException; +import org.yamcs.YConfiguration; +import org.yamcs.YamcsServer; +import org.yamcs.algorithms.AlgorithmExecutionContext; +import org.yamcs.mdb.AbstractDataDecoder; +import org.yamcs.parameter.Value; +import org.yamcs.tctm.AbstractPacketPreprocessor.TimeDecoderType; +import org.yamcs.tctm.AbstractPacketPreprocessor.TimeEpochs; +import org.yamcs.tctm.ccsds.time.CucTimeDecoder; +import org.yamcs.time.TimeCorrelationService; +import org.yamcs.utils.BitBuffer; +import org.yamcs.utils.ByteSupplier; +import org.yamcs.utils.ValueUtility; +import org.yamcs.xtce.CustomAlgorithm; +import org.yamcs.xtce.DataEncoding; + +/** + * Can be used in BinaryParameterEncoding to decode binary data directly to absolute times + *

+ * Unlike the pure XTCE based decoder, this one can use a time correlation service to convert between an on-board time + * and Yamcs time. + * + */ +public class TimeBinaryDecoder extends AbstractDataDecoder { + + final CucTimeDecoder timeDecoder; + final protected TimeEpochs timeEpoch; + final TimeCorrelationService tcoService; + final AlgorithmExecutionContext ctx; + + public TimeBinaryDecoder(CustomAlgorithm alg, AlgorithmExecutionContext ctx, Map conf) { + YConfiguration yc = YConfiguration.wrap(conf); + this.ctx = ctx; + TimeDecoderType type = yc.getEnum("type", TimeDecoderType.class, TimeDecoderType.CUC); + + timeDecoder = switch (type) { + case CUC -> { + int implicitPField = yc.getInt("implicitPField", -1); + int implicitPFieldCont = yc.getInt("implicitPFieldCont", -1); + yield new CucTimeDecoder(implicitPField, implicitPFieldCont); + } + default -> { + throw new UnsupportedOperationException("unknown time decoder type " + type); + } + }; + timeEpoch = yc.getEnum("epoch", TimeEpochs.class, TimeEpochs.GPS); + if (yc.containsKey("tcoService")) { + String tcoServiceName = yc.getString("tcoService"); + String yamcsInstance = ctx.getProcessorData().getYamcsInstance(); + tcoService = YamcsServer.getServer().getInstance(yamcsInstance) + .getService(TimeCorrelationService.class, tcoServiceName); + if (tcoService == null) { + throw new ConfigurationException( + "Cannot find a time correlation service with name " + tcoServiceName); + } + } else { + tcoService = null; + } + + } + + @Override + public Value extractRaw(DataEncoding de, BitBuffer buf) { + var suppl = new ByteSupplier() { + @Override + public byte getAsByte() { + return buf.getByte(); + } + }; + + long t; + if (tcoService != null) { + long obt = timeDecoder.decodeRaw(suppl); + /// FIXME: change to getHistoricalTime once we have a way of knowing the processor time (which should be the + /// replay time during replay) + t = tcoService.getTime(obt).getMillis(); + } else { + t = timeDecoder.decode(suppl); + } + + return ValueUtility.getTimestampValue(t); + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/archive/XtceTmRecorder.java b/yamcs-core/src/main/java/org/yamcs/archive/XtceTmRecorder.java index 523dfd82900..970fd3bf123 100644 --- a/yamcs-core/src/main/java/org/yamcs/archive/XtceTmRecorder.java +++ b/yamcs-core/src/main/java/org/yamcs/archive/XtceTmRecorder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; @@ -11,6 +12,7 @@ import org.yamcs.ConfigurationException; import org.yamcs.ContainerExtractionResult; import org.yamcs.InitException; +import org.yamcs.ProcessorConfig; import org.yamcs.Spec; import org.yamcs.Spec.OptionType; import org.yamcs.StandardTupleDefinitions; @@ -22,6 +24,7 @@ import org.yamcs.mdb.ContainerProcessingResult; import org.yamcs.mdb.Mdb; import org.yamcs.mdb.MdbFactory; +import org.yamcs.mdb.ProcessorData; import org.yamcs.mdb.XtceTmExtractor; import org.yamcs.time.TimeService; import org.yamcs.utils.parser.ParseException; @@ -197,7 +200,9 @@ class StreamRecorder implements StreamSubscriber, Runnable { if (async) { tmQueue = new LinkedBlockingQueue<>(100000); } - tmExtractor = new XtceTmExtractor(mdb); + var pdata = new ProcessorData(yamcsInstance, "XTCEPROC", mdb, new ProcessorConfig(), + Collections.emptyMap()); + tmExtractor = new XtceTmExtractor(mdb, pdata); // we do not want to get the containers which are included via container entry // we only want the inherited from the root diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/DataDecoderFactory.java b/yamcs-core/src/main/java/org/yamcs/mdb/DataDecoderFactory.java index ca8f85db968..b7a62f573db 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/DataDecoderFactory.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/DataDecoderFactory.java @@ -11,7 +11,7 @@ public class DataDecoderFactory { - public static DataDecoder get(Algorithm a) { + public static DataDecoder get(Algorithm a, ProcessorData pdata) { if (!(a instanceof CustomAlgorithm)) { throw new XtceProcessingException( "Unsupported algorithm: '" + a + "'. Only Java custom algorithms supported"); @@ -23,7 +23,8 @@ public static DataDecoder get(Algorithm a) { "Unsupported language for Data Decoder: '" + ca.getLanguage() + "'. Only Java supported"); } - return loadJavaAlgo(ca, null); + AlgorithmExecutionContext execCtx = new AlgorithmExecutionContext("DataDecoder", pdata, Integer.MAX_VALUE); + return loadJavaAlgo(ca, execCtx); } static T loadJavaAlgo(CustomAlgorithm alg, AlgorithmExecutionContext execCtx) { diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/DataEncoderFactory.java b/yamcs-core/src/main/java/org/yamcs/mdb/DataEncoderFactory.java index f2ad4c96674..92fb7c53cbe 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/DataEncoderFactory.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/DataEncoderFactory.java @@ -1,20 +1,23 @@ package org.yamcs.mdb; +import org.yamcs.algorithms.AlgorithmExecutionContext; import org.yamcs.xtce.Algorithm; import org.yamcs.xtce.CustomAlgorithm; public class DataEncoderFactory { - public static DataEncoder get(Algorithm a) { - if(!(a instanceof CustomAlgorithm)) { - throw new XtceProcessingException("Unsupported algorithm: '"+a+"'. Only Java custom algorithms supported"); + public static DataEncoder get(Algorithm a, ProcessorData pdata) { + if (!(a instanceof CustomAlgorithm)) { + throw new XtceProcessingException( + "Unsupported algorithm: '" + a + "'. Only Java custom algorithms supported"); } - CustomAlgorithm ca = (CustomAlgorithm)a; + CustomAlgorithm ca = (CustomAlgorithm) a; - if(!"java".equals(ca.getLanguage().toLowerCase())) { - throw new XtceProcessingException("Unsupported language for Data Encoder: '"+ca.getLanguage()+"'. Only Java supported"); + if (!"java".equals(ca.getLanguage().toLowerCase())) { + throw new XtceProcessingException( + "Unsupported language for Data Encoder: '" + ca.getLanguage() + "'. Only Java supported"); } - - return DataDecoderFactory.loadJavaAlgo(ca, null); + AlgorithmExecutionContext execCtx = new AlgorithmExecutionContext("DataEncoder", pdata, Integer.MAX_VALUE); + return DataDecoderFactory.loadJavaAlgo(ca, execCtx); } } diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/ParameterTypeProcessor.java b/yamcs-core/src/main/java/org/yamcs/mdb/ParameterTypeProcessor.java index 9b170d41f2c..e16e0a5f476 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/ParameterTypeProcessor.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/ParameterTypeProcessor.java @@ -42,9 +42,6 @@ /** * Responsible for converting between raw and engineering value by usage of calibrators or by simple type conversions. * - * - * @author nm - * */ public class ParameterTypeProcessor { ProcessorData pdata; @@ -337,7 +334,18 @@ private Value doFloatCalibration(ProcessingData processingData, FloatParameterTy private Value calibrateAbsoluteTime(ProcessingData processingData, AbsoluteTimeParameterType ptype, Value rawValue) { + ReferenceTime rtime = ptype.getReferenceTime(); + if (rtime == null) { + if (rawValue.getType() == Type.TIMESTAMP) { + return rawValue; + } else { + throw new IllegalStateException( + "Raw value type '" + rawValue.getType() + + "' cannot be converted to absolute time without a reference time"); + } + } + TimeEpoch epoch = rtime.getEpoch(); long offsetMillisec; diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/ProcessorData.java b/yamcs-core/src/main/java/org/yamcs/mdb/ProcessorData.java index fddd19c17d9..1eb89d441ab 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/ProcessorData.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/ProcessorData.java @@ -77,7 +77,7 @@ public class ProcessorData { Map typeOverrides = new HashMap<>(); private Set typeListeners = new CopyOnWriteArraySet<>(); - String yamcsInstance; + final String yamcsInstance; private ProcessorConfig processorConfig; @@ -272,21 +272,13 @@ public MatchCriteriaEvaluator getEvaluator(MatchCriteria mc) { } public DataDecoder getDataDecoder(DataEncoding de) { - DataDecoder dd = decoders.get(de); - if (dd == null) { - dd = DataDecoderFactory.get(de.getFromBinaryTransformAlgorithm()); - } - - return dd; + return decoders.computeIfAbsent(de, + de1 -> DataDecoderFactory.get(de1.getFromBinaryTransformAlgorithm(), this)); } public DataEncoder getDataEncoder(DataEncoding de) { - DataEncoder enc = encoders.get(de); - if (enc == null) { - enc = DataEncoderFactory.get(de.getToBinaryTransformAlgorithm()); - } - - return enc; + return encoders.computeIfAbsent(de, + de1 -> DataEncoderFactory.get(de1.getToBinaryTransformAlgorithm(), this)); } public Mdb getMdb() { diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/XtceTmProcessor.java b/yamcs-core/src/main/java/org/yamcs/mdb/XtceTmProcessor.java index 7c20d79f6f3..d6018a6c1b9 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/XtceTmProcessor.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/XtceTmProcessor.java @@ -1,5 +1,6 @@ package org.yamcs.mdb; +import java.util.Collections; import java.util.List; import org.yamcs.AbstractProcessorService; @@ -56,7 +57,8 @@ public XtceTmProcessor(Mdb mdb, ProcessorConfig pconfig) { this.processor = null; this.mdb = mdb; log = new Log(getClass()); - tmExtractor = new XtceTmExtractor(mdb, new ProcessorData("XTCEPROC", mdb, pconfig)); + var pdata = new ProcessorData(getYamcsInstance(), "XTCEPROC", mdb, pconfig, Collections.emptyMap()); + tmExtractor = new XtceTmExtractor(mdb,pdata); } @Override diff --git a/yamcs-core/src/main/java/org/yamcs/time/TimeCorrelationService.java b/yamcs-core/src/main/java/org/yamcs/time/TimeCorrelationService.java index c823f551b0c..1a1cdc593b6 100644 --- a/yamcs-core/src/main/java/org/yamcs/time/TimeCorrelationService.java +++ b/yamcs-core/src/main/java/org/yamcs/time/TimeCorrelationService.java @@ -62,8 +62,7 @@ * * * The time of flight can be fixed or computed by the {@link TimeOfFlightEstimator} by dynamically interpolating from - * data provided by a flight - * dynamics system. + * data provided by a flight dynamics system. *

* Computes {@code m} = gradient and {@code c} = offset such that *

@@ -72,10 +71,9 @@ * The determination of the gradient and offset is done using the least squares method. *

* The number of samples used for computing the coefficients is configurable and has to be minimum 2. - *

Accuracy and validity

- * Once the coefficients have been calculated, for each new sample received a deviation is calculated as the delta - * between the OBT computed using the coefficients and the OBT which is part of the sample (after adjusting for - * delays). The deviation is compared with the accuracy and validity parameters: + *

Accuracy and validity

Once the coefficients have been calculated, for each new sample received a deviation + * is calculated as the delta between the OBT computed using the coefficients and the OBT which is part of the sample + * (after adjusting for delays). The deviation is compared with the accuracy and validity parameters: * * * - *

Historical coefficients

- * The service keeps track of multiple intervals corresponding to different on-board time resets. At Yamcs startup - * the service loads a list of intervals from the tco table. + *

Historical coefficients

The service keeps track of multiple intervals corresponding to different on-board + * time resets. At Yamcs startup the service loads a list of intervals from the tco table. *

* If using the historical recording to insert some old data into the Yamcs, in order to get the correct coefficients * one has to know the approximate time when the data has been generated. * - *

Verify Only Mode

- * If the on-board clock is synchronized via a different method, this service can still be used to verify the - * synchronization. + *

Verify Only Mode

If the on-board clock is synchronized via a different method, this service can still be + * used to verify the synchronization. * *

* The method {@link #verify} will check the difference between the packet generation time and the expected generation @@ -107,9 +102,9 @@ * *

* To use this service there will be typically one component which adds samples using the - * {@link #addSample(long, Instant)} each time it - * receives a correlation sample from on-board. How the on-board system will send such samples is mission specific (for - * example the PUS protocol defines some specific time packets for this purpose). + * {@link #addSample(long, Instant)} each time it receives a correlation sample from on-board. How the on-board system + * will send such samples is mission specific (for example the PUS protocol defines some specific time packets for this + * purpose). *

* In addition there will be other components (preprocessors or other services) which can use the class to get a Yamcs * time associated to a specific OBT. @@ -117,11 +112,8 @@ * *

* This class is thread safe: the synchronised methods {@link #addSample} and {@link #reset} are the only one where the - * state is changed and thus the {@link #getTime(long)} can be used - * from multiple threads concurrently. + * state is changed and thus the {@link #getTime(long)} can be used from multiple threads concurrently. * - * @author nm - * */ public class TimeCorrelationService extends AbstractYamcsService implements SystemParametersProducer { static public final String TABLE_NAME = "tco_"; @@ -211,7 +203,7 @@ public void init(String yamcsInstance, String serviceName, YConfiguration config } else { tofEstimator = null; } - + this.timeService = YamcsServer.getTimeService(yamcsInstance); if (saveCoefficients) { tableName = TABLE_NAME + serviceName; diff --git a/yamcs-xtce/src/main/java/org/yamcs/xtce/DataEncoding.java b/yamcs-xtce/src/main/java/org/yamcs/xtce/DataEncoding.java index ff7696b4dbf..90eea72928a 100644 --- a/yamcs-xtce/src/main/java/org/yamcs/xtce/DataEncoding.java +++ b/yamcs-xtce/src/main/java/org/yamcs/xtce/DataEncoding.java @@ -14,10 +14,10 @@ *

* * DIFFERS_FROM_XTCE: XTCE defines known encodings for the usual types (e.g. twosComplement for signed integers) and - * allows - * a catch all using a BinaryDataEncoding with a custom algorithm. We consider this approach as flawed and inconsistent: - * whereas FloatDataEncoding converts from binary to float, IntegerDataEncoding converts from binary to integer, etc, - * the BinaryDataEncoding would convert from binary to anything and it cannot be known into what by just looking at it. + * allows a catch all using a BinaryDataEncoding with a custom algorithm. We consider this approach as flawed and + * inconsistent: whereas FloatDataEncoding converts from binary to float, IntegerDataEncoding converts from binary to + * integer, etc, the BinaryDataEncoding would convert from binary to anything and it cannot be known into what by just + * looking at it. * * Therefore in Yamcs we allow the catch all custom algorithm for all encodings and the BinaryDataEncoding can only * convert from binary to binary. @@ -85,9 +85,9 @@ public abstract class DataEncoding implements Serializable { } /** - * Returns the size in bits of data encoded according to this encoding. - * For some encodings like {@link StringDataEncoding} the size may be variable (depending on the data to be - * encoded). In this cases it returns -1. + * Returns the size in bits of data encoded according to this encoding. For some encodings like + * {@link StringDataEncoding} the size may be variable (depending on the data to be encoded). In this cases it + * returns -1. * * @return size in bits or -1 if the size is unknown */ @@ -124,8 +124,8 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE } /** - * parses the string into a java object of the correct type - * Has to match the DataEncodingDecoder (so probably it should be moved there somehow: TODO) + * parses the string into a java object of the correct type Has to match the DataEncodingDecoder (so probably it + * should be moved there somehow: TODO) */ public abstract Object parseString(String stringValue); @@ -193,6 +193,18 @@ public T setByteOrder(ByteOrder byteOrder) { return self(); } + public ByteOrder getByteOrder() { + return byteOrder; + } + + public Algorithm getFromBinaryTransformAlgorithm() { + return fromBinaryTransformAlgorithm; + } + + public Algorithm getToBinaryTransformAlgorithm() { + return toBinaryTransformAlgorithm; + } + @SuppressWarnings("unchecked") protected T self() { return (T) this; diff --git a/yamcs-xtce/src/main/java/org/yamcs/xtce/xml/XtceStaxReader.java b/yamcs-xtce/src/main/java/org/yamcs/xtce/xml/XtceStaxReader.java index 3230408ae25..60b29f333fb 100644 --- a/yamcs-xtce/src/main/java/org/yamcs/xtce/xml/XtceStaxReader.java +++ b/yamcs-xtce/src/main/java/org/yamcs/xtce/xml/XtceStaxReader.java @@ -674,8 +674,14 @@ private IncompleteType readAbsoluteTimeParameterType(SpaceSystem spaceSystem) th readEncoding(spaceSystem, typeBuilder); } else if (isEndElementWithName(ELEM_ABSOLUTE_TIME_PARAMETER_TYPE)) { if (typeBuilder.getReferenceTime() == null) { - throw new XMLStreamException("AbsoluteTimeParameterType without a reference time not supported", - xmlEvent.getLocation()); + if (!(typeBuilder.getEncoding() instanceof BinaryDataEncoding.Builder bdb) + || bdb.getFromBinaryTransformAlgorithm() == null) { + throw new XMLStreamException( + "AbsoluteTimeParameterType without a reference time not supported " + + "(except if it used a BinaryDataEncoding with an algorithm " + + "which could produce directly an absolute time)", + xmlEvent.getLocation()); + } } return incompleteType; } else { @@ -725,6 +731,7 @@ private TimeEpoch readEpoch() throws XMLStreamException { } } + // encoding used only for absolute time arguments or parameters private void readEncoding(SpaceSystem spaceSystem, AbsoluteTimeDataType.Builder ptype) throws XMLStreamException { log.trace(ELEM_ENCODING); @@ -759,6 +766,8 @@ private void readEncoding(SpaceSystem spaceSystem, AbsoluteTimeDataType.Builder< dataEncoding = readIntegerDataEncoding(spaceSystem); } else if (isStartElementWithName(ELEM_FLOAT_DATA_ENCODING)) { dataEncoding = readFloatDataEncoding(spaceSystem); + } else if (isStartElementWithName(ELEM_BINARY_DATA_ENCODING)) { + dataEncoding = readBinaryDataEncoding(spaceSystem); } else if (isEndElementWithName(ELEM_ENCODING)) { ptype.setEncoding(dataEncoding); return; From f7bb787972246cee6fbe3c6c1df468f5a7d83696 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Sat, 14 Sep 2024 06:05:57 +0200 Subject: [PATCH 02/31] added validity check window start<=end --- .../main/java/org/yamcs/xtce/CheckWindow.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/yamcs-xtce/src/main/java/org/yamcs/xtce/CheckWindow.java b/yamcs-xtce/src/main/java/org/yamcs/xtce/CheckWindow.java index 996559d4740..9405ea2142d 100644 --- a/yamcs-xtce/src/main/java/org/yamcs/xtce/CheckWindow.java +++ b/yamcs-xtce/src/main/java/org/yamcs/xtce/CheckWindow.java @@ -6,15 +6,13 @@ * From XTCE: Holds a time to stop checking and optional time to start checking and whether window is relative to * command release or last verifier. * - * @author nm - * */ public class CheckWindow implements Serializable { private static final long serialVersionUID = 2L; public enum TimeWindowIsRelativeToType { COMMAND_RELEASE, LAST_VERIFIER; - + static public TimeWindowIsRelativeToType fromXtce(String xtceAttr) { if ("timeLastVerifierPassed".equals(xtceAttr)) { return TimeWindowIsRelativeToType.LAST_VERIFIER; @@ -51,14 +49,21 @@ public String toXtce() { public CheckWindow(long timeToStartChecking, long timeToStopChecking, TimeWindowIsRelativeToType timeWindowIsRelativeTo) { - super(); + if (timeToStopChecking < timeToStartChecking) { + throw new IllegalArgumentException( + "timeToStopChecking has to be greater or equal than timeToStartChecking"); + } + + if (timeToStopChecking <= 0) { + throw new IllegalArgumentException( + "timeToStopChecking has to be strictly greater than 0"); + } + this.timeToStartChecking = timeToStartChecking; this.timeToStopChecking = timeToStopChecking; this.timeWindowIsRelativeTo = timeWindowIsRelativeTo; } - - public long getTimeToStartChecking() { return timeToStartChecking; } @@ -75,7 +80,6 @@ public boolean hasStart() { return timeToStartChecking != -1; } - public String toString() { return timeWindowIsRelativeTo + "[" + timeToStartChecking + "," + timeToStopChecking + "]"; } From 4da9241cbadd45e379381756d19f3d533dd27ddc Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Sat, 14 Sep 2024 06:07:34 +0200 Subject: [PATCH 03/31] doc update --- .../algorithms/AlgorithmExecutionResult.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/yamcs-core/src/main/java/org/yamcs/algorithms/AlgorithmExecutionResult.java b/yamcs-core/src/main/java/org/yamcs/algorithms/AlgorithmExecutionResult.java index 4dedd723dec..2fcbd7f8edb 100644 --- a/yamcs-core/src/main/java/org/yamcs/algorithms/AlgorithmExecutionResult.java +++ b/yamcs-core/src/main/java/org/yamcs/algorithms/AlgorithmExecutionResult.java @@ -3,21 +3,26 @@ import java.util.Arrays; import java.util.List; +import org.yamcs.commanding.VerificationResult; import org.yamcs.parameter.ParameterValue; import org.yamcs.parameter.RawEngValue; /** - * The result of the algorithm execution consists of: + * Describes the result of the algorithm execution, which consists of the following components: *

* - * @author nm - * + *

Command Verifier Return Value

+ *

+ * In older versions of Yamcs, the command verifier would return either a boolean value (True) indicating success or a + * String indicating failure. While this approach is still supported, the current preferred method is to use the custom + * {@link VerificationResult} class, which provides a more suitable representation of the verifier's result. + *

*/ public class AlgorithmExecutionResult { private final List inputValues; From 61468f0b7b2b5d36d804cfedbe0fb3b637fe50fd Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Sat, 21 Sep 2024 03:58:19 +0200 Subject: [PATCH 04/31] cleanup --- .../java/org/yamcs/tests/ParameterArchiveIntegrationTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/src/test/java/org/yamcs/tests/ParameterArchiveIntegrationTest.java b/tests/src/test/java/org/yamcs/tests/ParameterArchiveIntegrationTest.java index fc2633ffecc..c8f440a848e 100644 --- a/tests/src/test/java/org/yamcs/tests/ParameterArchiveIntegrationTest.java +++ b/tests/src/test/java/org/yamcs/tests/ParameterArchiveIntegrationTest.java @@ -489,9 +489,6 @@ public void testAggregatesWithSameTimestamps() throws Exception { values.clear(); page.iterator().forEachRemaining(values::add); - for (var v : values) { - System.out.println("v: " + Timestamps.toString(v.getGenerationTime())); - } assertEquals(16, values.size()); pv0 = values.get(0); pv1 = values.get(1); From a460b51262c29d089703df070c9b79c749eda664 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Sat, 21 Sep 2024 03:59:34 +0200 Subject: [PATCH 05/31] Fixes #934 --- .../main/java/org/yamcs/mdb/Subscription.java | 23 ++++++------ .../org/yamcs/mdb/ContainerEntryTest.java | 36 +++++++++++++++++++ yamcs-core/src/test/resources/mdb.yaml | 4 +++ .../test/resources/xtce/container-entry.xml | 29 +++++++++++++++ 4 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 yamcs-core/src/test/java/org/yamcs/mdb/ContainerEntryTest.java create mode 100644 yamcs-core/src/test/resources/xtce/container-entry.xml diff --git a/yamcs-core/src/main/java/org/yamcs/mdb/Subscription.java b/yamcs-core/src/main/java/org/yamcs/mdb/Subscription.java index ca4e579652f..464d35bc1fb 100644 --- a/yamcs-core/src/main/java/org/yamcs/mdb/Subscription.java +++ b/yamcs-core/src/main/java/org/yamcs/mdb/Subscription.java @@ -75,8 +75,8 @@ public SubscribedContainer addSequenceContainer(SequenceContainer containerDef) } /** - * Called in the cases when seq is part of other containers through aggregation. - * The parent container will need to know the size of this one so we add all entries of seq. + * Called in the cases when seq is part of other containers through aggregation. The parent container will need to + * know the size of this one so we add all entries of seq. * * @param seq */ @@ -99,12 +99,12 @@ public void addAll(SequenceContainer seq) { } public void addSequenceEntry(SequenceEntry se) { - addSequenceContainer(se.getSequenceContainer()); - SubscribedContainer subscr = containers.get(se.getSequenceContainer()); + SubscribedContainer subscr = addSequenceContainer(se.getSequenceContainer()); subscr.addEntry(se); SequenceContainer sctmp = se.getSequenceContainer(); + // if this entry's location is relative to the previous one, then we have to add also that one in the list if (se.getReferenceLocation() == SequenceEntry.ReferenceLocationType.PREVIOUS_ENTRY) { if (se.getIndex() > 0) { @@ -114,24 +114,27 @@ public void addSequenceEntry(SequenceEntry se) { do { sctmp = sctmp.getBaseContainer(); } while (sctmp != null && sctmp.getEntryList().size() == 0); - if (sctmp != null) { addSequenceEntry(sctmp.getEntryList().get(sctmp.getEntryList().size() - 1)); } } } - if ((se.getRepeatEntry() != null) && (se.getRepeatEntry().getCount() instanceof DynamicIntegerValue)) { - addParameter(((DynamicIntegerValue) se.getRepeatEntry().getCount()) - .getParameterInstanceRef().getParameter()); + + if ((se.getRepeatEntry() != null) && (se.getRepeatEntry().getCount() instanceof DynamicIntegerValue div)) { + addParameter(div.getParameterInstanceRef().getParameter()); } - if (se instanceof ArrayParameterEntry) { - ArrayParameterEntry ape = (ArrayParameterEntry) se; + + if (se instanceof ArrayParameterEntry ape) { for (IntegerValue iv : ape.getSize()) { if (iv instanceof DynamicIntegerValue) { addParameter(((DynamicIntegerValue) iv).getParameterInstanceRef().getParameter()); } } } + if (se instanceof ContainerEntry ce) { + addSequenceContainer(ce.getRefContainer()); + } + } /** diff --git a/yamcs-core/src/test/java/org/yamcs/mdb/ContainerEntryTest.java b/yamcs-core/src/test/java/org/yamcs/mdb/ContainerEntryTest.java new file mode 100644 index 00000000000..1b598b2386f --- /dev/null +++ b/yamcs-core/src/test/java/org/yamcs/mdb/ContainerEntryTest.java @@ -0,0 +1,36 @@ +package org.yamcs.mdb; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeAll; + +import org.junit.jupiter.api.Test; +import org.yamcs.ConfigurationException; +import org.yamcs.ProcessorConfig; +import org.yamcs.YConfiguration; +import org.yamcs.utils.TimeEncoding; + +public class ContainerEntryTest { + static Mdb mdb; + long now = TimeEncoding.getWallclockTime(); + XtceTmExtractor extractor; + MetaCommandProcessor metaCommandProcessor; + static ProcessorData pdata; + + @BeforeAll + public static void beforeClass() throws ConfigurationException { + YConfiguration.setupTest(null); + mdb = MdbFactory.createInstanceByConfig("container-entry"); + pdata = new ProcessorData("test", mdb, new ProcessorConfig()); + } + + @Test + public void testPartialSubscription() { + extractor = new XtceTmExtractor(mdb); + var p2 = mdb.getParameter("/ce/p2"); + extractor.startProviding(p2); + byte[] buf = new byte[] { 0, 1 }; + ContainerProcessingResult cpr = extractor.processPacket(buf, now, now, 0, mdb.getSequenceContainer("/ce/sc2")); + assertEquals(1, cpr.getParameterResult().getFirstInserted(p2).getEngValue().getUint32Value()); + } +} diff --git a/yamcs-core/src/test/resources/mdb.yaml b/yamcs-core/src/test/resources/mdb.yaml index 82e2768d353..18d5e0537cd 100644 --- a/yamcs-core/src/test/resources/mdb.yaml +++ b/yamcs-core/src/test/resources/mdb.yaml @@ -99,5 +99,9 @@ xtce-refsolver: args: file: "src/test/resources/xtce/refsolver3.xml" +container-entry: + - type: xtce + args: + file: "src/test/resources/xtce/container-entry.xml" \ No newline at end of file diff --git a/yamcs-core/src/test/resources/xtce/container-entry.xml b/yamcs-core/src/test/resources/xtce/container-entry.xml new file mode 100644 index 00000000000..4425820dd6e --- /dev/null +++ b/yamcs-core/src/test/resources/xtce/container-entry.xml @@ -0,0 +1,29 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ab3257413a2c19f2a02500a99e88a595e9af9cf4 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Mon, 23 Sep 2024 09:08:08 +0200 Subject: [PATCH 06/31] updated to rocksdb compatible to CPUs as old as core2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 83e49f6fb23..b052c84d97b 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ UTF-8 4.1.94.Final 3.19.6 - 9.4.0.6 + 9.4.0.10 2.0.9 From a89bb48b49ccc1bc8121382ee435d7ee93178ef7 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Fri, 27 Sep 2024 03:35:21 +0200 Subject: [PATCH 07/31] avoid unnecessary and costly compilation --- .../src/main/java/org/yamcs/yarch/streamsql/InExpression.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yamcs-core/src/main/java/org/yamcs/yarch/streamsql/InExpression.java b/yamcs-core/src/main/java/org/yamcs/yarch/streamsql/InExpression.java index 3c2e9f8b0aa..ac3411a95e8 100644 --- a/yamcs-core/src/main/java/org/yamcs/yarch/streamsql/InExpression.java +++ b/yamcs-core/src/main/java/org/yamcs/yarch/streamsql/InExpression.java @@ -45,7 +45,7 @@ public void addFilter(FilterableTarget tableStream) throws StreamSqlException { Set values = new HashSet(); for (int i = 1; i < children.length; i++) { Object cvalue; - if (children[1] instanceof ValueExpression) { + if (children[i].isConstant()) { cvalue = children[i].getConstantValue(); } else { CompiledExpression compexpr = children[i].compile(); From 4fa6c6b8eb50437e6e8b2238a95cd2de9546fab5 Mon Sep 17 00:00:00 2001 From: Nicolae Mihalache Date: Thu, 3 Oct 2024 00:26:11 +0200 Subject: [PATCH 08/31] command processing section in the docs --- .../processors/_images/command-processing.png | Bin 0 -> 151824 bytes .../processors/command-processing.rst | 127 ++++++++++++++++++ docs/server-manual/processors/index.rst | 1 + 3 files changed, 128 insertions(+) create mode 100644 docs/server-manual/processors/_images/command-processing.png create mode 100644 docs/server-manual/processors/command-processing.rst diff --git a/docs/server-manual/processors/_images/command-processing.png b/docs/server-manual/processors/_images/command-processing.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5562a853928b44d8ec0ae6ecf8e430399d1f8e GIT binary patch literal 151824 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Ɯ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 literal 0 HcmV?d00001 diff --git a/docs/server-manual/processors/command-processing.rst b/docs/server-manual/processors/command-processing.rst new file mode 100644 index 00000000000..72ec9b2ba22 --- /dev/null +++ b/docs/server-manual/processors/command-processing.rst @@ -0,0 +1,127 @@ +Command Processing +==================== + +This section provides a detailed description of how Yamcs processes commands based on MDB definitions, following a series of steps as outlined in the diagram below. + +The figure below provides an overview of the steps involved, followed by a more detailed description of each step. + +.. image:: _images/command-processing.png + :alt: Command Processing + :align: center + +**1. Command has container?** +When a command is received via API, the first step is to determine whether the command includes a container. The `allowContainerlessCommands` processor option is required for the command to be allowed without a container. + +If the command has a container, proceed to step 2, where the parameters used for container inheritance are generated. + +**2. Generate parameters** + +XTCE defines two methods for command inheritance conditions: using parameter conditions (this step) and argument assignments (step 3). P + +Parameter inheritance resembles telemetry, where conditions are based on parameter comparisons, allowing only equality conditions (as opposed to more general boolean conditions in TM inheritance). + +An example can be seen in the `CCSDS green book `__ + +Note that only equality conditions are allowed (whereas in TM inheritance general boolean conditions may be used). + +Yamcs will generate some parameter values according to the inheritance condition. The parameters may be used later in building the binary packet. Note that other than in the command building the value of these parameters are not published anywhere. The parameter generated are with both raw and engineering value. + +**3. Collect inherited arguments** + +The next step is to collect the arguments from all the ArgumentAssignments part of the inheritance conditions. This is similar with step 2. + +**4. Collect and check all arguments** + +In this step, all arguments — whether received from the user (via the API), inherited or from the default values — are gathered. All arguments are checked for validity. The value that is collected is the engineering value. The conversion to raw value will be performed only if the command has a container in step 8 below. + +**5. Command has container?** + +If the command has a container associated, the process moves to step 6, where the binary packet starts to being built. If not, the process skips directly to step 13 for verification. + +**6. For each entry in container** + +For commands with containers, each entry within the container is processed and inserted into the binary packet. The processing starts from the root container. +The type of entry determines the next steps in the flow. + +**7. Entry Type** + +Here, the type of entry within the container is determined: + +If the entry is an argument, the flow continues to step 8, where the argument is converted from engineering units to raw values. +If the entry has a fixed value, the process moves to step 11, where the fixed value binary is written to the packet. The fixed values are specified in binary, they do not need conversion. +If the entry is a parameter, the flow proceeds to step 10 to convert the parameter to binary. + +**8. Engineering to raw** + +For argument entries, the first step is to convert the engineering value to raw value. This may involve a calibration step. + +**9. Argument Raw to binary** + +The raw value is converted to a binary value according to the data encoding, possibly using an algorithm. + +**10. Parameter Raw to binary** + +Similarly, for parameter entries, the raw values are converted into binary format according to their data encoding. The parameter values used here are in priority those generated at step 2, or collected from the current values in the processor (from incoming TM). If no value is found for a parameter, an exception is thrown and the command processing stops. + +**11. Write entry binary to the packet** + +The converted binary values are written into the binary packet according to their absolute or relative position. + + +**12. Inherited containers** + +The steps 6-11 are repeated by traversing down the tree from the root container to the container associated to the command sent by the user, converting and inserting all entries. + +At this stage the command is built, ready to be sent. Yamcs will perform a few permissions checks: users with the `CommandOptions` system privilege are allowed to add different attributes to the command as well as disable transmission constraints and verifiers. Other users attempting to do that will be rejected. + +The API allows to issue a command with an option `dry_run=True`, case in which the processing will stop here and the prepared command including the binary and the collected argument values will be returned to the API user. + + +**13. Queue Command** + +At this step the command is inserted into the command queue and also into the Command History (this is the 'Q' ack in the command history). The queue where the command is inserted is determined by probing all the configures queues in order for these criteria: +- is the user allowed to enter commands in that queue +- is the command significance level appropriate for the queue +- is the command qualified name matching the patterns specified by the queue. This condition will satisfy if the queue has no pattern. + +If no queue matches the criteria, the last default queue will be used. Depending on the state of the selected queue, the following will happen: + +- If the selected queue is in state `DISABLED`, the command processing is immediately terminated. +- If the selected queue is in state `BLOCKED`, the command processing is suspended waiting for the queue to be enabled (or disabled and then the processing is terminated). +- If the selected queue is in state `ENABLED`, then the processing continues with the next step. + +**14. Transmission Constraints check** + +If the command has transmission constraints (and have not been disabled in the API request), the constrains will be checked possibly waiting a configured interval. The constraints typically involve checking some telemetry parameters. If no delay has been specified, the current value of the parameters are received from the processor cache and if the check fails, the command is failed. If the delay has been specified in the transmission constraint, the parameter is checked (if found in the cache) and if the check fails, a subscription will be created to the incoming parameters. + +If a successful check can be performed in the configured delay interval for all the constraints, then the command is released from the queue. + +Just before releasing the command from the queue, if the command has verifiers (and the verifiers have not been disabled in the API request), the verifiers are started. + +**15. Release Command** + +This step usually involves releasing the command into a stream (it corresponds to the 'R' ack in the command history). Note that the command releaser could be changed by the user in the processor.yaml. Here we describe what the default StreamTcCommandReleaser does. + +There maybe multiple streams where the command can be released. The instance configuration contains a list of TC streams (in the `streamConfig` section) each stream with a list of TC patterns specified. In addition, the user may specify via the API a particular stream where the command should be released. The streams are checked in order and the first stream that satisfies both conditions will be used. + +Finally, some services may insert themselves in the release list in front of the regular streams configured in the instance configuration. For example the Yamcs Gateway will do that to ensure that certain commands that it declare reach the nodes. Generally any component in Yamcs may define a command in MDB and add itself in the release list to make sure it receives that command. + + +**16. Send Command** + +If the command has been released into one of the regular streams, it ends up with the Link Manager. The Link Manager is the component that controls all the links declared in the instance configuration. Based on the `tcStream` property of each link, it has for each stream an ordered (the order is given by the link configuration) list of links that can send command from that stream. + +Once the Link Manager receives the command on a stream, it sequentially considers the *enabled* links associated with that stream. It attempts to send the command on each link in the order specified by the link configuration. Each link can either: + +1. decline sending the command passing it to the next link. +2. attempt to send the command and in this case the Link Manager will not attempt to use another link. + +If all the links have declined the offer to send the command (or were disabled), the Link Manager will fail the command with the error "no link available". + +Once a link has accepted to send the command, it is responsible to update the command hisotry with the Sent ('S') ack. If it failed to send the command it is also responsible for completing the command with failure. + +**17. Command Verification** + +As mentioned above, before the command has been released from the stream, all the verifiers are started. The command verifiers usually check for certain conditions in telemetry and populate the command history accordingly. Each verifier can at any time declare the command completion (either successfully or with failure) case in which all other running verifiers are immediately aborted. Similarly, the verifiers monitor the command history for command completion events generated by other sources (for example the link failing the command if it cannot send it) and they immediately abort in case the command has bene completed. + +Note that Yamcs does not enforce strict handling of command completion. For example, while a verifier may declare a command as failed, another component (such as a link) can later mark the same command as successful, updating the specific attribute in the Command History (which is a table in the database). diff --git a/docs/server-manual/processors/index.rst b/docs/server-manual/processors/index.rst index e510f9af1a6..1e7e464832f 100644 --- a/docs/server-manual/processors/index.rst +++ b/docs/server-manual/processors/index.rst @@ -11,6 +11,7 @@ Each processor is composed of a set of services with varying functionality. :caption: Table of Contents tm-processing + command-processing processor-configuration alarm-reporter algorithm-manager From f694c38c77a0200f2c4bfc1fc2522d712fc94f59 Mon Sep 17 00:00:00 2001 From: Matthieu Melcot Date: Wed, 2 Oct 2024 14:37:34 +0200 Subject: [PATCH 09/31] Throw exception when TM stream is missing in configuration --- yamcs-core/src/main/java/org/yamcs/StreamTmPacketProvider.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yamcs-core/src/main/java/org/yamcs/StreamTmPacketProvider.java b/yamcs-core/src/main/java/org/yamcs/StreamTmPacketProvider.java index d086afc8fbe..abe38be39a7 100644 --- a/yamcs-core/src/main/java/org/yamcs/StreamTmPacketProvider.java +++ b/yamcs-core/src/main/java/org/yamcs/StreamTmPacketProvider.java @@ -66,6 +66,9 @@ private void readStreamConfig(String procName) { for (String streamName : streams) { TmStreamConfigEntry sce = streamConfig.getTmEntry(streamName); + if (sce == null) + throw new ConfigurationException("Cannot find TM stream configuration for '" + streamName + "'"); + SequenceContainer rootContainer; rootContainer = sce.getRootContainer(); if (rootContainer == null) { From 85beddbcd8f75769d279dbe2cf048a9652790b8e Mon Sep 17 00:00:00 2001 From: Fabian Diet Date: Wed, 4 Sep 2024 23:37:16 +0200 Subject: [PATCH 10/31] Fix serialization of Any exception detail message in HTTP response --- .../java/org/yamcs/http/CallObserver.java | 38 ++++++++++++-- .../org/yamcs/http/HttpRequestHandler.java | 5 +- .../java/org/yamcs/http/RouteHandler.java | 52 ++++++++++++++++++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/yamcs-core/src/main/java/org/yamcs/http/CallObserver.java b/yamcs-core/src/main/java/org/yamcs/http/CallObserver.java index bd0c70cad73..a46cf713176 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/CallObserver.java +++ b/yamcs-core/src/main/java/org/yamcs/http/CallObserver.java @@ -1,5 +1,8 @@ package org.yamcs.http; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -152,7 +155,7 @@ private ChannelFuture sendMessageResponse(T responseMsg) { MediaType contentType = ctx.deriveTargetContentType(); if (contentType != MediaType.JSON) { ctx.reportStatusCode(OK.code()); - return HttpRequestHandler.sendMessageResponse(ctx.nettyContext, req, OK, responseMsg); + return sendMessageResponse(OK, responseMsg); } else { ByteBuf body = ctx.nettyContext.alloc().buffer(); try (ByteBufOutputStream channelOut = new ByteBufOutputStream(body)) { @@ -161,7 +164,7 @@ private ChannelFuture sendMessageResponse(T responseMsg) { body.writeCharSequence(str, StandardCharsets.UTF_8); } catch (IOException e) { body.release(); - HttpResponseStatus status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + HttpResponseStatus status = INTERNAL_SERVER_ERROR; ctx.reportStatusCode(status.code()); return HttpRequestHandler.sendPlainTextError(ctx.nettyContext, req, status, e.toString()); } @@ -173,7 +176,7 @@ private ChannelFuture sendMessageResponse(T responseMsg) { } } - static ChannelFuture sendError(RouteContext ctx, HttpException t) { + private ChannelFuture sendError(RouteContext ctx, HttpException t) { if (t instanceof InternalServerErrorException) { log.error("Internal server error while handling call", t); } else if (log.isDebugEnabled()) { @@ -181,6 +184,33 @@ static ChannelFuture sendError(RouteContext ctx, HttpException t) { } ExceptionMessage msg = t.toMessage(); ctx.reportStatusCode(t.getStatus().code()); - return HttpRequestHandler.sendMessageResponse(ctx.nettyContext, ctx.nettyRequest, t.getStatus(), msg); + return sendMessageResponse(t.getStatus(), msg); + } + + private ChannelFuture sendMessageResponse(HttpResponseStatus status, T responseMsg) { + ByteBuf body = ctx.nettyContext.alloc().buffer(); + MediaType contentType = HttpRequestHandler.getAcceptType(ctx.nettyRequest); + + try { + if (contentType == MediaType.PROTOBUF) { + try (ByteBufOutputStream channelOut = new ByteBufOutputStream(body)) { + responseMsg.writeTo(channelOut); + } + } else if (contentType == MediaType.PLAIN_TEXT) { + body.writeCharSequence(responseMsg.toString(), StandardCharsets.UTF_8); + } else { // JSON by default + contentType = MediaType.JSON; + String str = ctx.printJson(responseMsg); + body.writeCharSequence(str, StandardCharsets.UTF_8); + } + } catch (IOException e) { + return HttpRequestHandler.sendPlainTextError(ctx.nettyContext, ctx.nettyRequest, INTERNAL_SERVER_ERROR, + e.toString()); + } + HttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, body); + response.headers().set(CONTENT_TYPE, contentType.toString()); + response.headers().set(CONTENT_LENGTH, body.readableBytes()); + + return HttpRequestHandler.sendResponse(ctx.nettyContext, ctx.nettyRequest, response); } } diff --git a/yamcs-core/src/main/java/org/yamcs/http/HttpRequestHandler.java b/yamcs-core/src/main/java/org/yamcs/http/HttpRequestHandler.java index 3fdac89ff2d..afe7d378517 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/HttpRequestHandler.java +++ b/yamcs-core/src/main/java/org/yamcs/http/HttpRequestHandler.java @@ -195,6 +195,9 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E public static ChannelFuture sendMessageResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponseStatus status, T responseMsg) { + // Note: don't use this method when there's a possibility of JSON/Any message serialization + // The used JSON printer does not have type definitions registered. + ByteBuf body = ctx.alloc().buffer(); MediaType contentType = getAcceptType(req); @@ -269,7 +272,7 @@ private void cleanPipeline(ChannelPipeline pipeline) { * Returns the Accept header if present and not set to ANY or Content-Type header if present or JSON if none of the * headers is present or the Accept is present and set to ANY. */ - private static MediaType getAcceptType(HttpRequest req) { + static MediaType getAcceptType(HttpRequest req) { String acceptType = req.headers().get(ACCEPT); if (acceptType != null) { MediaType r = MediaType.from(acceptType); diff --git a/yamcs-core/src/main/java/org/yamcs/http/RouteHandler.java b/yamcs-core/src/main/java/org/yamcs/http/RouteHandler.java index 4dc01a0a375..da95c475cdb 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/RouteHandler.java +++ b/yamcs-core/src/main/java/org/yamcs/http/RouteHandler.java @@ -1,5 +1,12 @@ package org.yamcs.http; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledFuture; @@ -11,6 +18,7 @@ import java.util.regex.Pattern; import org.yamcs.YamcsServer; +import org.yamcs.api.ExceptionMessage; import org.yamcs.http.audit.AuditLog; import org.yamcs.logging.Log; @@ -19,10 +27,16 @@ import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.Message; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; @Sharable public class RouteHandler extends SimpleChannelInboundHandler { @@ -163,7 +177,43 @@ private void handleException(RouteContext ctx, Throwable t) { } else { log.warn("{}: Responding '{}': {}", ctx, e.getStatus(), e.getMessage()); } - CallObserver.sendError(ctx, e); + + if (t instanceof InternalServerErrorException) { + log.error("Internal server error while handling call", t); + } else if (log.isDebugEnabled()) { + log.debug("User error while handling call", t); + } + ExceptionMessage msg = e.toMessage(); + ctx.reportStatusCode(e.getStatus().code()); + sendMessageResponse(ctx, e.getStatus(), msg); + } + + private ChannelFuture sendMessageResponse(RouteContext ctx, HttpResponseStatus status, + T responseMsg) { + ByteBuf body = ctx.nettyContext.alloc().buffer(); + MediaType contentType = HttpRequestHandler.getAcceptType(ctx.nettyRequest); + + try { + if (contentType == MediaType.PROTOBUF) { + try (ByteBufOutputStream channelOut = new ByteBufOutputStream(body)) { + responseMsg.writeTo(channelOut); + } + } else if (contentType == MediaType.PLAIN_TEXT) { + body.writeCharSequence(responseMsg.toString(), StandardCharsets.UTF_8); + } else { // JSON by default + contentType = MediaType.JSON; + String str = ctx.printJson(responseMsg); + body.writeCharSequence(str, StandardCharsets.UTF_8); + } + } catch (IOException e) { + return HttpRequestHandler.sendPlainTextError(ctx.nettyContext, ctx.nettyRequest, INTERNAL_SERVER_ERROR, + e.toString()); + } + HttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, body); + response.headers().set(CONTENT_TYPE, contentType.toString()); + response.headers().set(CONTENT_LENGTH, body.readableBytes()); + + return HttpRequestHandler.sendResponse(ctx.nettyContext, ctx.nettyRequest, response); } private void createAuditRecord(RouteContext ctx, Message message) { From 91bef04df077d9125a42ac0c9bca5e25c345f03e Mon Sep 17 00:00:00 2001 From: Fabian Diet Date: Mon, 9 Sep 2024 19:58:59 +0200 Subject: [PATCH 11/31] Add java-expression highlighting --- yamcs-web/src/main/webapp/package-lock.json | 544 +++++++++--------- yamcs-web/src/main/webapp/package.json | 1 + .../algorithm-detail.component.ts | 4 + .../algorithm-detail.component.ts | 4 + 4 files changed, 277 insertions(+), 276 deletions(-) diff --git a/yamcs-web/src/main/webapp/package-lock.json b/yamcs-web/src/main/webapp/package-lock.json index 33a49bf0da8..cd704c83ab0 100644 --- a/yamcs-web/src/main/webapp/package-lock.json +++ b/yamcs-web/src/main/webapp/package-lock.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "^18.0.1", "@angular/router": "^18.0.1", "@angular/service-worker": "^18.0.1", + "@codemirror/lang-java": "^6.0.1", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.5", "@codemirror/state": "^6.4.1", @@ -59,13 +60,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", - "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", + "version": "0.1802.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.3.tgz", + "integrity": "sha512-WQ2AmkUKy1bqrDlNfozW8+VT2Tv/Fdmu4GIXps3ytZANyAKiIvTzmmql2cRCXXraa9FNMjLWNvz+qolDxWVdYQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.3", "rxjs": "7.8.1" }, "engines": { @@ -75,17 +76,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.1.tgz", - "integrity": "sha512-ANsTWKjIlEvJ6s276TbwnDhkoHhQDfsNiRFUDRGBZu94UNR78ImQZSyKYGHJOeQQH6jpBtraA1rvW5WKozAtlw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.3.tgz", + "integrity": "sha512-uUQba0SIskKORHcPayt7LpqPRKD//48EW92SgGHEArn2KklM+FSYBOA9OtrJeZ/UAcoJpdLDtvyY4+S7oFzomg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/build-webpack": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular/build": "18.2.1", + "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/build-webpack": "0.1802.3", + "@angular-devkit/core": "18.2.3", + "@angular/build": "18.2.3", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -96,7 +97,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.1", + "@ngtools/webpack": "18.2.3", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -138,8 +139,8 @@ "tslib": "2.6.3", "vite": "5.4.0", "watchpack": "2.4.1", - "webpack": "5.93.0", - "webpack-dev-middleware": "7.3.0", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -211,13 +212,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.1.tgz", - "integrity": "sha512-xOP9Hxkj/mWYdMTa/8uNxFTv7z+3UiGdt4VAO7vetV5qkU/S9rRq8FEKviCc2llXfwkhInSgeeHpWKdATa+YIQ==", + "version": "0.1802.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.3.tgz", + "integrity": "sha512-/Nixv9uAg6v/OPoZa0PB0zi+iezzBkgLrnrJnestny5B536l9WRtsw97RjeQDu+x2BClQsxNe8NL2A7EvjVD6w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.3", "rxjs": "7.8.1" }, "engines": { @@ -231,9 +232,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", - "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.3.tgz", + "integrity": "sha512-vbFs+ofNK9OWeMIcFarFjegXVklhtSdLTEFKZ9trDVr8alTJdjI9AiYa6OOUTDAyq0hqYxV26xlCisWAPe7s5w==", "dev": true, "license": "MIT", "dependencies": { @@ -259,13 +260,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.1.tgz", - "integrity": "sha512-2t/q0Jcv7yqhAzEdNgsxoGSCmPgD4qfnVOJ7EJw3LNIA+kX1CmtN4FESUS0i49kN4AyNJFAI5O2pV8iJiliKaw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.3.tgz", + "integrity": "sha512-N3tRAzBW2yWQhebvc1Ha18XTMSXOQTfr8HNjx7Fasx0Fg1tNyGR612MJNZw6je/PqyItKeUHOhztvFMfCQjRyg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.3", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -278,9 +279,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.1.tgz", - "integrity": "sha512-jit452yuE6DMVV09E6RAjgapgw64mMVH31ccpPvMDekzPsTuP3KNKtgRFU/k2DFhYJvyczM1AqqlgccE/JGaRw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.3.tgz", + "integrity": "sha512-rIATopHr83lYR0X05buHeHssq9CGw0I0YPIQcpUTGnlqIpJcQVCf7jCFn4KGZrE9V55hFY3MD4S28njlwCToQQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -289,18 +290,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1" + "@angular/core": "18.2.3" } }, "node_modules/@angular/build": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.1.tgz", - "integrity": "sha512-HwzjB+I31cAtjTTbbS2NbayzfcWthaKaofJlSmZIst3PN+GwLZ8DU0DRpd/xu5AXkk+DoAIWd+lzUIaqngz6ow==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.3.tgz", + "integrity": "sha512-USrD2Zvcb1te2dnqhH7JZ5XeJDg/t7fjUHR4f93vvMrnrncwCjLoHbHpz01HCHfcIVRgsYUdAmAi1iG7vpak7w==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.3", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -362,9 +363,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.1.tgz", - "integrity": "sha512-6y4MmpEPXze6igUHkLsBUPkxw32F8+rmW0xVXZchkSyGlFgqfh53ueXoryWb0qL4s5enkNY6AzXnKAqHfPNkVQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.3.tgz", + "integrity": "sha512-lUcpYTxPZuntJ1FK7V2ugapCGMIhT6TUDjIGgXfS9AxGSSKgwr8HNs6Ze9pcjYC44UhP40sYAZuiaFwmE60A2A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -379,18 +380,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz", - "integrity": "sha512-SomUFDHanY4o7k3XBGf1eFt4z1h05IGJHfcbl2vxoc0lY59VN13m/pZsD2AtpqtJTzLQT02XQOUP4rmBbGoQ+Q==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.3.tgz", + "integrity": "sha512-40258vuliH6+p8QSByZe5EcIXSj0iR3PNF6yuusClR/ByToHOnmuPw7WC+AYr0ooozmqlim/EjQe4/037OUB3w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/core": "18.2.3", + "@angular-devkit/schematics": "18.2.3", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.1", + "@schematics/angular": "18.2.3", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -413,9 +414,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.1.tgz", - "integrity": "sha512-N0ZJO1/iU9UhprplZRPvBcdRgA/i6l6Ng5gXs5ymHBJ0lxsB+mDVCmC4jISjR9gAWc426xXwLaOpuP5Gv3f/yg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.3.tgz", + "integrity": "sha512-NFL4yXXImSCH7i1xnHykUjHa9vl9827fGiwSV2mnf7LjSUsyDzFD8/54dNuYN9OY8AUD+PnK0YdNro6cczVyIA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -424,14 +425,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1", + "@angular/core": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.1.tgz", - "integrity": "sha512-5e9ygKEcsBoV6xpaGKVrtsLxLETlrM0oB7twl4qG/xuKYqCLj8cRQMcAKSqDfTPzWMOAQc7pHdk+uFVo/8dWHA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.3.tgz", + "integrity": "sha512-Il3ljs0j1GaYoqYFdShjUP1ryck5xTOaA8uQuRgqwU0FOwEDfugSAM3Qf7nJx/sgxTM0Lm/Nrdv2u6i1gZWeuQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -440,7 +441,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1" + "@angular/core": "18.2.3" }, "peerDependenciesMeta": { "@angular/core": { @@ -449,9 +450,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.1.tgz", - "integrity": "sha512-D+Qba0r6RfHfffzrebGYp54h05AxpkagLjit/GczKNgWSP1gIgZxSfi88D+GvFmeWvZxWN1ecAQ+yqft9hJqWg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.3.tgz", + "integrity": "sha512-BcmqYKnkcJTkGjuPztClZNQve7tdI290J5F3iZBx6c7/vaw8EU8EGZtpWYZpgiVn5S6jhcKyc1dLF9ggO9vftg==", "dev": true, "license": "MIT", "dependencies": { @@ -473,14 +474,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.1", + "@angular/compiler": "18.2.3", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.1.tgz", - "integrity": "sha512-9KrSpJ65UlJZNXrE18NszcfOwb5LZgG+LYi5Doe7amt218R1bzb3trvuAm0ZzMaoKh4ugtUCkzEOd4FALPEX6w==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.3.tgz", + "integrity": "sha512-VGhMJxj7d0rYpqVfQrcGRB7EE/BCziotft/I/YPl6bOMPSAvMukG7DXQuJdYpNrr62ks78mlzHlZX/cdmB9Prw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -494,9 +495,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.1.tgz", - "integrity": "sha512-T7z8KUuj2PoPxrMrAruQVJha+x4a9Y6IrKYtArgOQQlTwCEJuqpVYuOk5l3fwWpHE9bVEjvgkAMI1D5YXA/U6w==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.3.tgz", + "integrity": "sha512-+OBaAH0e8hue9eyLnbgpxg1/X9fps6bwXECfJ0nL5BDPU5itZ428YJbEnj5bTx0hEbqfTRiV4LgexdI+D9eOpw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -505,32 +506,32 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.1.tgz", - "integrity": "sha512-JI4oox9ELNdDVg0uJqCwgyFoK4XrowV14wSoNpGhpTLModRg3eDS6q+8cKn27cjTQRZvpReyYSTfiZMB8j4eqQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.3.tgz", + "integrity": "sha512-bTZ1O7s0uJqKdd9ImCleRS9Wg6yVy2ZXchnS5ap2gYJx51MJgwOM/fL6is0OsovtZG/UJaKK5FeEqUUxNqZJVA==", "license": "MIT", "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0" } }, "node_modules/@angular/material": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.1.tgz", - "integrity": "sha512-DBSJGqLttT9vYpLGWTuuRoOKd1mNelS0jnNo7jNZyMpjcGfuhNzmPtYiBkXfNsAl7YoXoUmX8+4uh1JZspQGqA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.3.tgz", + "integrity": "sha512-JFfvXaMHMhskncaxxus4sDvie9VYdMkfYgfinkLXpZlPFyn1IzjDw0c1BcrcsuD7UxQVZ/v5tucCgq1FQfGRpA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.1", + "@angular/cdk": "18.2.3", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -539,9 +540,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz", - "integrity": "sha512-hQABX7QotGmCIR3EhCBCDh5ZTvQao+JkuK5CCw2G1PkRfJMBwEpjNqnyhz41hZhWiGlucp9jgbeypppW+mIQEw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.3.tgz", + "integrity": "sha512-M2ob4zN7tAcL2mx7U6KnZNqNFPFl9MlPBE0FrjQjIzAjU0wSYPIJXmaPu9aMUp9niyo+He5iX98I+URi2Yc99g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -550,9 +551,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.1", - "@angular/common": "18.2.1", - "@angular/core": "18.2.1" + "@angular/animations": "18.2.3", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3" }, "peerDependenciesMeta": { "@angular/animations": { @@ -561,9 +562,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.1.tgz", - "integrity": "sha512-tYJHtshbaKrtnRA15k3vrveSVBqkVUGhINvGugFA2vMtdTOfhfPw+hhzYrcwJibgU49rHogCfI9mkIbpNRYntA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.3.tgz", + "integrity": "sha512-nWi9ZxN4KpbJkttIckFO1PCoW0+gb/18xFO+JWyLBAtcbsudj/Mv0P/fdOaSfQdLkPhZfORr3ZcfiTkhmuGyEg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -572,16 +573,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/compiler": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1" + "@angular/common": "18.2.3", + "@angular/compiler": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3" } }, "node_modules/@angular/router": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.1.tgz", - "integrity": "sha512-gVyqW6fYnG7oq1DlZSXJMQ2Py2dJQB7g6XVtRcYB1gR4aeowx5N9ws7PjqAi0ih91ASq2MmP4OlSSWLq+eaMGg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.3.tgz", + "integrity": "sha512-fvD9eSDIiIbeYoUokoWkXzu7/ZaxlzKPUHFqX1JuKuH5ciQDeT/d7lp4mj31Bxammhohzi3+z12THJYsCkj/iQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -590,16 +591,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.1.tgz", - "integrity": "sha512-Is4arGy+4HjyvALmR/GsWI4SwXYVJ1IkauAgxPsQKvWLNHdX7a/CEgEEVQGXq96H46QX9O2OcW69PnPatmJIXg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.3.tgz", + "integrity": "sha512-KplaBYhhwsM3gPeOImfDGhAknN+BIcZJkHl8YRnhoUEFHsTZ8LTV02C4LWQL3YTu3pK+uj/lPMKi1CA37cXQ8g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -611,8 +612,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1" + "@angular/common": "18.2.3", + "@angular/core": "18.2.3" } }, "node_modules/@babel/code-frame": { @@ -1027,14 +1028,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -1057,13 +1058,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -1238,13 +1239,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2446,17 +2447,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2465,13 +2466,13 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2481,9 +2482,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "license": "MIT", "dependencies": { @@ -2514,9 +2515,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", - "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.1.tgz", + "integrity": "sha512-iBfKbyIoXS1FGdsKcZmnrxmbc8VcbMrSgD7AVrsnX+WyAYjmUDWvE93dt5D874qS4CCVu4O1JpbagHdXbbLiOw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2525,6 +2526,16 @@ "@lezer/common": "^1.1.0" } }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", @@ -3031,15 +3042,15 @@ "license": "MIT" }, "node_modules/@inquirer/checkbox": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", - "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3062,16 +3073,16 @@ } }, "node_modules/@inquirer/core": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", - "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.1.0.tgz", + "integrity": "sha512-RZVfH//2ytTjmaBIzeKT1zefcQZzuruwkpTwwbe/i2jTl4o9M+iML5ChULzz6iw1Ok8iUBBsRCjY2IEbD8Ft4w==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "@types/mute-stream": "^0.0.4", - "@types/node": "^22.1.0", + "@types/node": "^22.5.2", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-spinners": "^2.9.2", @@ -3087,14 +3098,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", - "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" }, "engines": { @@ -3102,14 +3113,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", - "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3127,42 +3138,42 @@ } }, "node_modules/@inquirer/input": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", - "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/number": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", - "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/password": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", - "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -3192,14 +3203,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", - "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3207,15 +3218,15 @@ } }, "node_modules/@inquirer/search": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", - "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3223,15 +3234,15 @@ } }, "node_modules/@inquirer/select": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", - "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3240,9 +3251,9 @@ } }, "node_modules/@inquirer/type": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", - "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.3.tgz", + "integrity": "sha512-xUQ14WQGR/HK5ei+2CvgcwoH9fQ4PgPGmVFSN0pc1+fVyDL3MREhyAY7nxEErSu6CkllBM3D7e3e+kOvtu+eIg==", "dev": true, "license": "MIT", "dependencies": { @@ -3271,9 +3282,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -3508,6 +3519,17 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/java": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.2.tgz", + "integrity": "sha512-3j8X70JvYf0BZt8iSRLXLkt0Ry1hVUgH6wT32yBxH/Xi55nW2VMhc1Az4SKwu4YGSmxCm1fsqDDcHTuFjC8pmg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.4.17", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz", @@ -3724,9 +3746,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.1.tgz", - "integrity": "sha512-v86U3jOoy5R9ZWe9Q0LbHRx/IBw1lbn0ldBU+gIIepREyVvb9CcH/vAyIb2Fw1zaYvvfG1OyzdrHyW8iGXjdnQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.3.tgz", + "integrity": "sha512-DDuBHcu23qckt43SexBJaPEIeMc/HKaFOidILZM9D4gU4C9VroMActdR218dvQ802QfL0S46t5Ykz8ENprIfjA==", "dev": true, "license": "MIT", "engines": { @@ -4327,9 +4349,9 @@ ] }, "node_modules/@rollup/wasm-node": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.21.0.tgz", - "integrity": "sha512-CqLrY1oc68dyB44h4qfAa/4LM+R+xvqaJSTBV0hWeLXiIdXhgrHlaalXOTrL5vWz+mgnyzlUgy3bhTkZjKt1LQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.21.2.tgz", + "integrity": "sha512-AJCfdXkpe5EX+jfWOMYuFl3ZomTQyfx4V4geRmChdTwAo05FdpnobwqtYn0mo7Mf1qVN7mniI7kdG98vKDVd2g==", "dev": true, "license": "MIT", "dependencies": { @@ -4347,14 +4369,14 @@ } }, "node_modules/@schematics/angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.1.tgz", - "integrity": "sha512-bBV7I+MCbdQmBPUFF4ECg37VReM0+AdQsxgwkjBBSYExmkErkDoDgKquwL/tH7stDCc5IfTd0g9BMeosRgDMug==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.3.tgz", + "integrity": "sha512-whSON70z9HYb4WboVXmPFE/RLKJJQLWNzNcUyi8OSDZkQbJnYgPp0///n738m26Y/XeJDv11q1gESy+Zl2AdUw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/core": "18.2.3", + "@angular-devkit/schematics": "18.2.3", "jsonc-parser": "3.3.1" }, "engines": { @@ -4531,28 +4553,6 @@ "@types/google.visualization": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4634,9 +4634,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, "license": "MIT", "dependencies": { @@ -5633,9 +5633,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001659", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz", + "integrity": "sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA==", "dev": true, "funding": [ { @@ -6399,13 +6399,13 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6654,16 +6654,16 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.18", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz", + "integrity": "sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -6877,9 +6877,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -7278,9 +7278,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -8486,9 +8486,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "license": "MIT", "dependencies": { @@ -8605,9 +8605,9 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -8870,9 +8870,9 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -9033,9 +9033,9 @@ } }, "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz", + "integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -9436,9 +9436,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -9815,9 +9815,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", "dev": true, "license": "MIT", "optional": true, @@ -10595,9 +10595,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -11563,13 +11563,6 @@ "dev": true, "license": "MIT" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -11910,9 +11903,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12134,9 +12127,9 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -13325,13 +13318,12 @@ "license": "MIT" }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -13340,7 +13332,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -13373,9 +13365,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/yamcs-web/src/main/webapp/package.json b/yamcs-web/src/main/webapp/package.json index 77c83e01cce..b7598ebcc91 100644 --- a/yamcs-web/src/main/webapp/package.json +++ b/yamcs-web/src/main/webapp/package.json @@ -26,6 +26,7 @@ "@angular/platform-browser-dynamic": "^18.0.1", "@angular/router": "^18.0.1", "@angular/service-worker": "^18.0.1", + "@codemirror/lang-java": "^6.0.1", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.5", "@codemirror/state": "^6.4.1", diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.ts index a6bf3b48430..901ef4d2415 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.ts @@ -1,5 +1,6 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { indentWithTab } from '@codemirror/commands'; +import { java } from '@codemirror/lang-java'; import { javascript } from '@codemirror/lang-javascript'; import { python } from '@codemirror/lang-python'; import { EditorState, Extension } from '@codemirror/state'; @@ -84,6 +85,9 @@ export class AlgorithmDetailComponent implements AfterViewInit { extensions.push(theme); switch (this.algorithm.language.toLowerCase()) { + case 'java-expression': + extensions.push(java()); + break; case 'javascript': extensions.push(javascript()); break; diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/mdb/algorithms/algorithm-detail/algorithm-detail.component.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/mdb/algorithms/algorithm-detail/algorithm-detail.component.ts index a2f3b65b66e..1fea3dc28da 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/mdb/algorithms/algorithm-detail/algorithm-detail.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/mdb/algorithms/algorithm-detail/algorithm-detail.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { java } from '@codemirror/lang-java'; import { javascript } from '@codemirror/lang-javascript'; import { python } from '@codemirror/lang-python'; import { EditorState, Extension } from '@codemirror/state'; @@ -41,6 +42,9 @@ export class AlgorithmDetailComponent { ]; switch (this.algorithm.language.toLowerCase()) { + case 'java-expression': + extensions.push(java()); + break; case 'javascript': extensions.push(javascript()); break; From 6267fc30b297de97a5daa220dc7c28b28f9ec0a0 Mon Sep 17 00:00:00 2001 From: Fabian Diet Date: Sat, 7 Sep 2024 12:12:31 +0200 Subject: [PATCH 12/31] Advance event level migration For a long time we've had two conflicting event levels: * INFO, WARNING, ERROR * INFO, WATCH, WARNING, DISTRESS, CRITICAL, SEVERE. This commit advances the deprecation away from "INFO, WARNING, ERROR". In a future commit, probably we'll need to split EventSeverity enum, between the DB and the API, to apply a correct conversion of old events. The current commit serves only to add a new state with serialization id 4 (temporarily named "WARNING_NEW"), so that Protobuf clients can start to make preparations for a future migration step. --- .../algorithms/AlgorithmManagerTest.java | 2 +- .../proto/yamcs/protobuf/events/events.proto | 29 ++++++++++++++++--- .../java/org/yamcs/http/api/EventsApi.java | 13 ++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/tests/src/test/java/org/yamcs/algorithms/AlgorithmManagerTest.java b/tests/src/test/java/org/yamcs/algorithms/AlgorithmManagerTest.java index bd01ed5d8ba..ee4f338040b 100644 --- a/tests/src/test/java/org/yamcs/algorithms/AlgorithmManagerTest.java +++ b/tests/src/test/java/org/yamcs/algorithms/AlgorithmManagerTest.java @@ -204,7 +204,7 @@ public void testFunctions() throws Exception { String defaultSource = "CustomAlgorithm"; for (EventSeverity sev : EventSeverity.values()) { - if (sev == EventSeverity.ERROR) { + if (sev == EventSeverity.ERROR || sev == EventSeverity.WARNING_NEW) { continue; } diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/events/events.proto b/yamcs-api/src/main/proto/yamcs/protobuf/events/events.proto index 502cd727638..db4505ac4aa 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/events/events.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/events/events.proto @@ -10,16 +10,37 @@ import "google/protobuf/timestamp.proto"; message Event { + // The severity levels, in order are: + // INFO, WATCH, WARNING, DISTRESS, CRITICAL, SEVERE. + // + // A migration is underway to fully move away from the legacy + // INFO, WARNING, ERROR levels. enum EventSeverity { INFO = 0; WARNING = 1; - ERROR = 2; - //the levels below are compatible with XTCE - // we left the 4 out since it could be used - // for warning if we ever decide to get rid of the old ones + // Legacy, avoid use. + ERROR = 2 [deprecated=true]; WATCH = 3; + + // Placeholder for future WARNING constant. + // (correctly sorted between WATCH and DISTRESS) + // + // Most clients can ignore, this state is here + // to give Protobuf clients (Python Client, Yamcs Studio) + // the time to add a migration for supporting both WARNING + // and WARNING_NEW (Protobuf serializes the number). + // + // Then in a later phase, we move from: + // WARNING=1, WARNING_NEW=4 + // + // To: + // WARNING_OLD=1, WARNING=4 + // + // (which is a transparent change to JSON clients) + WARNING_NEW = 4; + DISTRESS = 5; CRITICAL = 6; SEVERE = 7; diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java b/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java index 9af667f5b1e..34299e8969a 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java +++ b/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java @@ -201,6 +201,10 @@ public void createEvent(Context ctx, CreateEventRequest request, Observer if (severity == null) { throw new BadRequestException("Unsupported severity: " + request.getSeverity()); } + if (severity == EventSeverity.ERROR) { + log.warn("DEPRECATION WARNING: Do not create events with ERROR level, " + + "this will be removed in a future release."); + } eventb.setSeverity(severity); } else { eventb.setSeverity(EventSeverity.INFO); @@ -523,7 +527,14 @@ public static Event fromDbEvent(Db.Event other) { evb.setMessage(other.getMessage()); } if (other.hasSeverity()) { - evb.setSeverity(other.getSeverity()); + if (other.getSeverity() == EventSeverity.ERROR) { + evb.setSeverity(EventSeverity.SEVERE); + } else if (other.getSeverity() == EventSeverity.WARNING_NEW) { + // Temporary during WARNING -> WARNING_NEW migration + evb.setSeverity(EventSeverity.WARNING); + } else { + evb.setSeverity(other.getSeverity()); + } } if (other.hasCreatedBy()) { evb.setCreatedBy(other.getCreatedBy()); From f723b28a95c4432130fbfdd604ca9e9be7457f50 Mon Sep 17 00:00:00 2001 From: Fabian Diet Date: Wed, 4 Sep 2024 12:23:10 +0200 Subject: [PATCH 13/31] Add event query filter --- docs/http-api/.gitignore | 6 +- docs/http-api/conf.py | 7 +- docs/http-api/filtering.rst | 263 +++ docs/http-api/overview.rst | 21 - docs/http-api/partial-responses.rst | 20 + docs/http-api/websocket.rst | 2 +- docs/server-manual/general/time.rst | 15 +- .../src/main/proto/yamcs/api/exception.proto | 14 + .../activities/activities_service.proto | 2 +- .../protobuf/archive/index_service.proto | 2 +- .../proto/yamcs/protobuf/audit/audit.proto | 7 +- .../protobuf/events/events_service.proto | 125 +- .../yamcs/protobuf/timeline/timeline.proto | 83 +- .../java/org/yamcs/client/InstanceFilter.java | 2 +- .../java/org/yamcs/http/api/EventFilter.java | 66 + .../yamcs/http/api/EventFilterFactory.java | 62 + .../java/org/yamcs/http/api/EventsApi.java | 108 +- .../java/org/yamcs/http/api/InstancesApi.java | 46 +- .../org/yamcs/http/api/ParameterListsApi.java | 2 +- .../http/api/SubscribeEventsObserver.java | 76 + .../java/org/yamcs/utils/parser/Filter.java | 353 ++++ .../org/yamcs/utils/parser/FilterParser.java | 800 +++++++++- .../org/yamcs/utils/parser/FilterParser.jj | 314 +++- .../utils/parser/FilterParserConstants.java | 57 +- .../parser/FilterParserTokenManager.java | 263 ++- .../utils/parser/IncorrectTypeException.java | 22 + .../utils/parser/InvalidPatternException.java | 22 + .../org/yamcs/utils/parser/TokenMgrError.java | 286 ++-- .../utils/parser/UnknownFieldException.java | 22 + .../yamcs/utils/parser/ast/AndExpression.java | 27 + .../yamcs/utils/parser/ast/Comparator.java | 13 + .../yamcs/utils/parser/ast/Comparison.java | 29 + .../java/org/yamcs/utils/parser/ast/Node.java | 6 + .../yamcs/utils/parser/ast/OrExpression.java | 27 + .../utils/parser/ast/UnaryExpression.java | 46 + .../yamcs/utils/parser/javacc-invocation.sh | 3 + .../org/yamcs/utils/FilterParserTest.java | 343 +++- .../src/main/java/org/yamcs/web/WebApi.java | 52 - .../main/java/org/yamcs/web/WebPlugin.java | 5 +- .../yamcs/web/api/ParseFilterObserver.java | 49 + .../main/java/org/yamcs/web/api/WebApi.java | 183 +++ .../src/main/java/org/yamcs/web/db/Query.java | 92 ++ .../main/java/org/yamcs/web/db/QueryDb.java | 161 ++ .../main/proto/yamcs/protobuf/web/web.proto | 140 ++ yamcs-web/src/main/webapp/package-lock.json | 333 ++-- .../webapp/projects/lezer-filter/.gitignore | 6 + .../projects/lezer-filter/package-lock.json | 1415 +++++++++++++++++ .../webapp/projects/lezer-filter/package.json | 19 + .../projects/lezer-filter/rollup.config.js | 15 + .../projects/lezer-filter/src/filter.grammar | 58 + .../projects/lezer-filter/src/highlight.js | 15 + .../projects/lezer-filter/test/comments.txt | 8 + .../lezer-filter/test/comparisons.txt | 38 + .../projects/lezer-filter/test/literals.txt | 23 + .../projects/lezer-filter/test/logical.txt | 21 + .../projects/lezer-filter/test/parens.txt | 7 + .../projects/lezer-filter/test/strings.txt | 23 + .../projects/lezer-filter/test/test-filter.js | 17 + .../projects/lezer-filter/test/texts.txt | 35 + .../webapp-sdk/src/lib/client/HttpError.ts | 9 +- .../src/lib/client/WebSocketCall.ts | 17 +- .../webapp-sdk/src/lib/client/YamcsClient.ts | 51 +- .../webapp-sdk/src/lib/client/index.ts | 8 +- .../webapp-sdk/src/lib/client/types/events.ts | 10 +- .../webapp-sdk/src/lib/client/types/web.ts | 39 + .../column-chooser.component.html | 8 +- .../column-chooser.component.ts | 8 +- .../lib/components/filter/FilterErrorMark.ts | 6 + .../src/lib/components/filter/cmSetup.ts | 178 +++ .../filter/filter-input.component.css | 35 + .../filter/filter-input.component.html | 13 + .../filter/filter-input.component.ts | 172 ++ .../filter/filter-textarea.component.css | 3 + .../filter/filter-textarea.component.html | 1 + .../filter/filter-textarea.component.ts | 148 ++ .../src/lib/components/filter/highlight.js | 15 + .../src/lib/components/filter/lang-filter.ts | 14 + .../src/lib/components/filter/parser.js | 21 + .../src/lib/components/filter/parser.terms.js | 15 + .../message-bar/message-bar.component.css | 6 + .../search-filter2.component.css | 26 + .../search-filter2.component.html | 29 + .../search-filter2.component.ts | 144 ++ .../table-top/table-top.component.css | 21 + .../table-top/table-top.component.html | 12 + .../table-top/table-top.component.ts | 22 + .../webapp-sdk/src/lib/webapp-sdk.module.ts | 8 + .../projects/webapp-sdk/src/public-api.ts | 4 + .../webapp-sdk/src/styles/data-table.css | 10 +- .../projects/webapp-sdk/src/styles/form.css | 5 +- .../webapp-sdk/src/styles/material-theme.scss | 1 + .../projects/webapp-sdk/src/styles/styles.css | 12 + .../command-history-list.component.html | 4 +- .../create-event-dialog.component.ts | 1 + .../create-event-query-dialog.component.html | 48 + .../create-event-query-dialog.component.ts | 54 + .../edit-event-query-dialog.component.html | 48 + .../edit-event-query-dialog.component.ts | 58 + .../src/app/events/event-list/completions.ts | 108 ++ .../event-list/event-list.component.css | 4 + .../event-list/event-list.component.html | 204 ++- .../events/event-list/event-list.component.ts | 91 +- .../events/event-list/events.datasource.ts | 23 +- .../event-query-list.component.html | 48 + .../event-query-list.component.ts | 74 + .../events-page-tabs.component.css | 10 + .../events-page-tabs.component.html | 29 + .../events-page-tabs.component.ts | 17 + .../webapp/src/app/events/events.resolvers.ts | 24 + .../webapp/src/app/events/events.routes.ts | 9 + .../export-events-dialog.component.html | 38 +- .../export-events-dialog.component.ts | 8 +- .../file-transfer-list.component.ts | 10 +- .../file-transfer/file-transfer.resolvers.ts | 9 + .../app/file-transfer/file-transfer.routes.ts | 3 +- .../src/app/shared/codemirror/highlight.js | 15 + .../src/app/shared/codemirror/lang-filter.ts | 14 + .../src/app/shared/codemirror/parser.js | 21 + .../src/app/shared/codemirror/parser.terms.js | 15 + .../instance-page/instance-page.component.ts | 17 +- yamcs-web/src/main/webapp/tsconfig.json | 3 +- 121 files changed, 7524 insertions(+), 831 deletions(-) create mode 100644 docs/http-api/filtering.rst create mode 100644 docs/http-api/partial-responses.rst create mode 100644 yamcs-core/src/main/java/org/yamcs/http/api/EventFilter.java create mode 100644 yamcs-core/src/main/java/org/yamcs/http/api/EventFilterFactory.java create mode 100644 yamcs-core/src/main/java/org/yamcs/http/api/SubscribeEventsObserver.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/Filter.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/IncorrectTypeException.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/InvalidPatternException.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/UnknownFieldException.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/AndExpression.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparator.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparison.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Node.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/OrExpression.java create mode 100644 yamcs-core/src/main/java/org/yamcs/utils/parser/ast/UnaryExpression.java create mode 100755 yamcs-core/src/main/java/org/yamcs/utils/parser/javacc-invocation.sh delete mode 100644 yamcs-web/src/main/java/org/yamcs/web/WebApi.java create mode 100644 yamcs-web/src/main/java/org/yamcs/web/api/ParseFilterObserver.java create mode 100644 yamcs-web/src/main/java/org/yamcs/web/api/WebApi.java create mode 100644 yamcs-web/src/main/java/org/yamcs/web/db/Query.java create mode 100644 yamcs-web/src/main/java/org/yamcs/web/db/QueryDb.java create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/.gitignore create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/package-lock.json create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/package.json create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/rollup.config.js create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/src/filter.grammar create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/src/highlight.js create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/comments.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/comparisons.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/literals.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/logical.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/parens.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/strings.txt create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/test-filter.js create mode 100644 yamcs-web/src/main/webapp/projects/lezer-filter/test/texts.txt create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/web.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/FilterErrorMark.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/cmSetup.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.css create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.css create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/highlight.js create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/lang-filter.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.js create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.terms.js create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.css create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/completions.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-query-list/event-query-list.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-query-list/event-query-list.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/events-page-tabs/events-page-tabs.component.css create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/events-page-tabs/events-page-tabs.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/events-page-tabs/events-page-tabs.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/events/events.resolvers.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/file-transfer.resolvers.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/shared/codemirror/highlight.js create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/shared/codemirror/lang-filter.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/shared/codemirror/parser.js create mode 100644 yamcs-web/src/main/webapp/projects/webapp/src/app/shared/codemirror/parser.terms.js diff --git a/docs/http-api/.gitignore b/docs/http-api/.gitignore index f3320383244..d4a3581992e 100644 --- a/docs/http-api/.gitignore +++ b/docs/http-api/.gitignore @@ -1,4 +1,4 @@ +/_build/ .autogen -*.rst -!/overview.rst -!/websocket.rst +/*/**/*.rst +/index.rst diff --git a/docs/http-api/conf.py b/docs/http-api/conf.py index e6fb69d4bbd..be0330c35a1 100644 --- a/docs/http-api/conf.py +++ b/docs/http-api/conf.py @@ -61,4 +61,9 @@ ) yamcs_api_destdir = "." yamcs_api_title = "Yamcs HTTP API" -yamcs_api_additional_docs = ["overview.rst", "websocket.rst"] +yamcs_api_additional_docs = [ + "overview.rst", + "filtering.rst", + "partial-responses.rst", + "websocket.rst", +] diff --git a/docs/http-api/filtering.rst b/docs/http-api/filtering.rst new file mode 100644 index 00000000000..17e8ea888cd --- /dev/null +++ b/docs/http-api/filtering.rst @@ -0,0 +1,263 @@ +Filtering +========= + +Some list methods provide a ``filter`` option. This option can be use to provide a query expression to filter based on the fields of each list item. Methods providing this option allow to use ``POST`` in addition to ``GET``, to avoid encoding of lengthy queries in the query parameter. + +The filter syntax allows for two kinds of search: text search, and field search. + + +Text Search +----------- + +A single word is matched against the full resource. It is up to the specific resource implementation to determine which fields are considered for this comparison, usually all textual fields. The search is case-insensitive, exact, and may be partial. + +For example, search resources that match the text `wombat`: + +.. code-block:: text + + wombat + +To find resources that match both the text `icy` and the text `wombat` (at the same time), provide them both separated by whitespace: + +.. code-block:: text + :caption: Separated by space + + icy wombat + +.. code-block:: text + :caption: Separated by newline + + icy + wombat + +Search terms may be enclosed in double quotes, which allows the search to include special characters. + +The previous example is identical to: + +.. code-block:: text + + "icy" + "wombat" + +If you would rather search for the exact sequence `icy wombat`, use double quotes around the full search term: + +.. code-block:: text + + "icy wombat" + +To search resources that `do not` match the text `wombat`, negate the term by prefixing with the minus sign: + +.. code-block:: text + + -wombat + + +Logical Operators +^^^^^^^^^^^^^^^^^ + +Logical operators ``AND``, ``OR`` and ``NOT`` can be used to form more complicated queries. These operators must be specified in uppercase, else they are considered to be search terms. + +For example: + +.. code-block:: text + + wombat OR hippo + +.. code-block:: text + + NOT hippo OR (icy AND wombat) + +NOT has highest precedence, followed by OR, then AND. Where needed, use parenthesis to avoid any confusion. + +Use of the `AND` operator is optional, as this is the default behavior when multiple terms are provided. + + +Line Comments +^^^^^^^^^^^^^ + +Queries can span any number of lines. Lines can be commented out using the `--` prefix. + +The following example searches for resources that textually match with both `wombat` and `gorilla`. + +.. code-block:: text + + wombat + --hippo + gorilla + + +Field Search +------------ + +Each filterable resource defines a number of fields that can be used to filter directly. This allows for better targeting than with text search, and will generally perform better. + +The available filterable fields vary from one resource to another, and are documented on their respective pages (for example, see: :doc:`events/list-events`). + +Field search requires a comparison query of the form: `FIELD OPERATOR VALUE`. For example, to filter resources that have the field `foo` set to `wombat`, use any of the following: + +.. code-block:: text + + foo=wombat + foo = wombat + foo = "wombat" + +Each field has a specified type: string, number, boolean, binary or enum. The following sections describe the operators for each of these types. + + +String Field Comparison Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following operators can be used in string field comparisons. + +.. list-table:: + :widths: 25 25 50 + + * - = + - foo = "wombat" + - The field `foo` equals `wombat` + * - != + - foo != "wombat" + - The field `foo` does not equal `wombat` + * - < + - foo < "wombat" + - The field `foo` is alphabetically before `wombat` + * - <= + - foo <= "wombat" + - The field `foo` equals `wombat`, or is alphabetically before `wombat` + * - > + - foo > "wombat" + - The field `foo` is alphabetically after `wombat` + * - >= + - foo >= "wombat" + - The field `foo` equals `wombat`, or is alphabetically after `wombat` + * - : + - foo:"wombat" + - The field `foo` contains the substring `wombat` + * - =~ + - foo =~ "bat$" + - The field `foo` ends with the substring `bat` + * - !~ + - foo !~ "bat$" + - The field `foo` does not end with the substring `bat` + +The operators `=~` and `!~` allow to match the field against the provide regular expression. The match is unanchored, so use the prefix `^` and the suffix `$` if you want to match the full field value. + +Regular expressions are case-sensitive. To enable case-insensitive matching, you can use an embedded flag expression: + +.. code-block:: + + foo =~ "(?i)bat$" + +Regular expressions must be double-quoted. For the other operators, double quotes are optional, unless you want to match special characters. + + +Number Field Comparison Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following operators can be used in number field comparisons: + +.. list-table:: + :widths: 25 25 50 + + * - = + - foo = 123.45 + - The field `foo` equals `wombat` + * - != + - foo != 123.45 + - The field `foo` does not equal `123.45` + * - < + - foo < 123.45 + - The field `foo` is smaller than `123.45` + * - <= + - foo <= 123.45 + - The field `foo` equals `123.45`, or is smaller than `123.45` + * - > + - foo > 123.45 + - The field `foo` is greater than `123.45` + * - >= + - foo >= 123.45 + - The field `foo` equals `123.45`, or is greater than `123.45` + +The comparison value may be double-quoted. + + +Boolean Field Comparison Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following operators can be used in boolean field comparisons: + +.. list-table:: + :widths: 25 25 50 + + * - = + - foo = true + - The field `foo` is `true` + * - != + - foo != true + - The field `foo` is not `true` (so, null or false) + +The comparison values `true` and `false` are case-insensitive, and may be double-quoted. + + +Binary Field Comparison Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following operators can be used in binary field comparisons: + +.. list-table:: + :widths: 25 25 50 + + * - = + - foo = aabb + - The field `foo` is two bytes long, `0xAA` and `0xBB` + * - != + - foo != aabb + - The field `foo` does not match `0xAABB` + * - : + - foo:aabb + - The field `foo` contains the binary `0xAABB` + +The provided hexstring is case-insensitive, and may be double-quoted. + + +Enum Field Comparison Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Assume a field `foo` of the following enum type: + +.. code-block:: java + + enum Severity { + INFO, + WATCH, + WARNING, + DISTRESS, + CRITICAL, + SEVERE; + } + +The following operators can be used in enum field comparisons. + +.. list-table:: + :widths: 25 25 50 + + * - = + - foo = INFO + - The field `foo` equals `INFO` + * - != + - foo != INFO + - The field `foo` does not equal `INFO` + * - < + - foo < WATCH + - The field `foo` is before `WATCH`, using enum order + * - <= + - foo <= WATCH + - The field `foo` is `WATCH`, or before `WATCH`, using enum order + * - > + - foo > WATCH + - The field `foo` is after `WATCH`, using enum order + * - >= + - foo >= WATCH + - The field `foo` is `WATCH`, or after `WATCH`, using enum order + +The provided enum constant is case-insensitive, and may be double-quoted. diff --git a/docs/http-api/overview.rst b/docs/http-api/overview.rst index dd917426f01..58c02a24cfb 100644 --- a/docs/http-api/overview.rst +++ b/docs/http-api/overview.rst @@ -54,27 +54,6 @@ Cross-origin Resource Sharing (CORS) allows access to the Yamcs HTTP API from a CORS is off by default on Yamcs Server, but available through configuration. -.. rubric:: Response Filtering - -Responses can be filtered using the query parameter ``fields``, or alternatively by setting the HTTP header ``X-Yamcs-Fields``. This is usually not needed, because when unspecified all fields are returned. Some clients may anyway want to be explicit, for optimizing the message sizes. - -Field names are applicable to the top-level response message, and multiple fields can be separated by commas. Methods that return a list of messages apply the mask to each of the listed resources. Field paths can be of arbitrary depth separated by dots. Only the last part can refer to a repeated field. - -Some examples: - -Return information on the `simulator` instance, but include only the ``name`` and ``state`` fields: - -.. code-block:: - - curl 'localhost:8090/api/instances/simulator?fields=name,state' - -Return a list of all instances, but include only the ``name`` and ``state`` fields: - -.. code-block:: - - curl 'localhost:8090/api/instances?fields=name,state' - - .. rubric:: JSON All API methods are designed for JSON-over-HTTP. Matching type definitions in this documentation are written in TypeScript syntax because of its high readability. Note however that we do not currently mark parameters as optional (``?``). diff --git a/docs/http-api/partial-responses.rst b/docs/http-api/partial-responses.rst new file mode 100644 index 00000000000..badc0dca411 --- /dev/null +++ b/docs/http-api/partial-responses.rst @@ -0,0 +1,20 @@ +Partial Responses +================= + +To reduce the size of an HTTP response message, it is possible to restrict returned fields by providing the query parameter ``fields``, or alternatively by setting the HTTP header ``X-Yamcs-Fields``. This is also called a `field mask`. + +Field names are applicable to the top-level response message, and multiple fields can be separated by commas. Methods that return a list of messages apply the mask to each of the listed resources. Field paths can be of arbitrary depth separated by dots. Only the last part can refer to a repeated field. + +Some examples: + +Return information on the `simulator` instance, but include only the ``name`` and ``state`` fields: + +.. code-block:: + + curl 'localhost:8090/api/instances/simulator?fields=name,state' + +Return a list of all instances, but include only the ``name`` and ``state`` fields: + +.. code-block:: + + curl 'localhost:8090/api/instances?fields=name,state' diff --git a/docs/http-api/websocket.rst b/docs/http-api/websocket.rst index 2b7519f9c0f..34d0533971b 100644 --- a/docs/http-api/websocket.rst +++ b/docs/http-api/websocket.rst @@ -192,7 +192,7 @@ To confirm your request, Yamcs will first send you a reply that looks somewhat l As the client, we note that the server has assigned the call identifier ``3`` to this subscription. .. note:: - The property ``@type`` is an artifact generated by Yamcs JSON backend. It specifies the equivalent Protobuf message type of the ``data`` object (Yamcs generates JSON based on Protobuf definitions). You should be able to ignore this property because we enforce each message ``type`` to be using only a single ``data`` message. + The property ``@type`` is an artifact generated by Yamcs JSON backend. It specifies the equivalent Protobuf message type of the ``data`` object (Yamcs generates JSON based on Protobuf definitions). You may ignore this property because each message ``type`` uses only a single ``data`` message. Next we receive continued time updates, each in a WebSocket frame: diff --git a/docs/server-manual/general/time.rst b/docs/server-manual/general/time.rst index bdd3b37808c..f6f7636067a 100644 --- a/docs/server-manual/general/time.rst +++ b/docs/server-manual/general/time.rst @@ -7,7 +7,7 @@ The text below documents several aspects of working with time in Yamcs. Time Encoding ------------- -Yamcs uses 8 bytes signed integers (long in java) for representing milliseconds since 1-Jan-1970 00:00:00 TAI, including leap seconds. The Yamcs time in milliseconds is the UNIX time (in milliseconds) + leap seconds. +Yamcs uses signed eight-byte integers (long in Java) for representing milliseconds since 1-Jan-1970 00:00:00 TAI, including leap seconds. The Yamcs time in milliseconds is the UNIX time (in milliseconds) + leap seconds. To convert accurately between TAI and UTC, a leap second table is used. Yamcs parses this information from the configuration file :file:`etc/UTC-TAI.history` in :abbr:`IERS (International Earth Rotation and Reference Systems Service)` format: @@ -29,11 +29,11 @@ The user is responsible for updating manually this file if it changes (when new #. Download the latest :file:`UTC-TAI.history` file from IERS. #. Deploy this file to :file:`etc/UTC-TAI.history` under the Yamcs directory. #. Restart Yamcs -#. Verify the leap second table in the :doc:`Admin Area <../web-interface/admin/leap-seconds>`. +#. Verify the leap second table in :doc:`Admin Area <../web-interface/admin/leap-seconds>`. Yamcs also has a high resolution time implemented in the class :javadoc:`org.yamcs.time.Instant`. This is represented as :math:`8+4` bytes milliseconds and picoseconds of the millisecond. It is not widely used - in Java it is not even easily possible to get the time with a resolution better than millisecond. -The higher resolution time is sent sometimes from external systems - for example a Ground Station may timestamp the incoming packets with a microsecond or nanosecond precise time (derived from an atomic clock) - this time is available as the Earth Reception Time via the yamcs-sle plugin. +The higher resolution time is sent sometimes from external systems. For example a Ground Station may timestamp the incoming packets with a microsecond or nanosecond precise time (derived from an atomic clock). This time is available as the Earth Reception Time via the yamcs-sle plugin. The class that allows working with times, offering conversion functionality between the Yamcs time and UTC is :javadoc:`org.yamcs.utils.TimeEncoding`. @@ -41,7 +41,7 @@ The class that allows working with times, offering conversion functionality betw Wall clock time --------------- -The wall clock time is the computer time converted to Yamcs format. The ``getWallclockTime()`` function in ``TimeEncoding`` can be used to get the current wallclock time. In practice, in 2023, the following is true: +The wall clock time is the computer time converted to Yamcs format. The ``getWallclockTime()`` function in ``TimeEncoding`` can be used to get the current wallclock time. In practice, in 2024, the following is true: .. code-block:: java @@ -55,7 +55,7 @@ Mission Time The mission time in Yamcs is the *current* time. For a realtime mission that would be the wall clock time. For a simulation it would be the simulation time. -The mission time is specific to a Yamcs instance and is given by the :javadoc:`org.yamcs.time.TimeService` configured in that instance. The time service is configured using the ``timeService`` keyword in the :file:`etc/yamcs.{instance}.yaml`. +The mission time is specific to a Yamcs instance and is given by the :javadoc:`org.yamcs.time.TimeService` configured in that instance. The time service is configured using the ``timeService`` keyword in :file:`etc/yamcs.{instance}.yaml`. There are two time services implemented as part of standard Yamcs: @@ -84,10 +84,9 @@ The generation time is the time when the data has been generated. For telemetry packets, it is set by the pre-processor, normally with a time extracted from the packet. However it can be set to the mission time if the ``useLocalGenerationTime`` option is set to true. -The timeEncoding option is used on the TM links to configure how to extract the time from the packet - which means how to covert a number (or more numbers) extracted from the packet to a Yamcs time. The various options for time decoding are documented in the :doc:`../links/packet-preprocessor` +The timeEncoding option is used on the TM links to configure how to extract the time from the packet - which means how to convert a number (or more numbers) extracted from the packet to a Yamcs time. The various options for time decoding are documented in the :doc:`../links/packet-preprocessor` - -The spacecrafts which have no mean to synchronize time (e.g. no access to GPS) will usually use a free running on-board clock (initialized to 0 at startup) to timestamp the packets. In these cases, the on-board time needs to be correlated with the mission time. The :doc:`../services/instance/time-correlation` can be used for this purpose. +Spacecrafts that have no means to synchronize time (e.g. no access to GPS) will usually use a free running on-board clock (initialized to 0 at startup) to timestamp the packets. In these cases, the on-board time needs to be correlated with the mission time. The :doc:`../services/instance/time-correlation` can be used for this purpose. Finally, the TM links have an option ``updateSimulationTime`` which can be used to set the mission time to the time extracted from the packet. This works if the SimulationTimeService is used. diff --git a/yamcs-api/src/main/proto/yamcs/api/exception.proto b/yamcs-api/src/main/proto/yamcs/api/exception.proto index 20bc19a789f..2fb77dbf16d 100644 --- a/yamcs-api/src/main/proto/yamcs/api/exception.proto +++ b/yamcs-api/src/main/proto/yamcs/api/exception.proto @@ -15,3 +15,17 @@ message ExceptionMessage { string msg = 3; google.protobuf.Any detail = 4; } + +message FilterSyntaxException { + // Begin line of the token where the exception occurs + int32 beginLine = 1; + + // Begin column of the token where the exception occurs + int32 beginColumn = 2; + + // End line of the token where the exception occurs + int32 endLine = 3; + + // End column of the token where the exception occurs + int32 endColumn = 4; +} diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/activities/activities_service.proto b/yamcs-api/src/main/proto/yamcs/protobuf/activities/activities_service.proto index 8063c62786e..56691ff0951 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/activities/activities_service.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/activities/activities_service.proto @@ -210,7 +210,7 @@ message ListActivitiesRequest { optional string instance = 1; // The maximum number of returned records per page. Choose this value too high - // and you risk hitting the maximum response size limit enforced by the server. + // and you risk hitting the maximum response size limit enforced by the server. // Default: ``100`` optional int32 limit = 2; diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/archive/index_service.proto b/yamcs-api/src/main/proto/yamcs/protobuf/archive/index_service.proto index 3924f30c8e3..f625679c0de 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/archive/index_service.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/archive/index_service.proto @@ -218,7 +218,7 @@ message ListCompletenessIndexRequest { // The maximum number of returned entries. Choose this value too high and you risk hitting // the maximum response size limit enforced by the server. Default: ``1000``. // Note that in general it is advised to control the size of the response via ``mergeTime``, - // rather than via ``limit``. + // rather than via ``limit``. optional int32 limit = 2; // Filter the lower bound of the index entries. Specify a date string in ISO 8601 format. diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/audit/audit.proto b/yamcs-api/src/main/proto/yamcs/protobuf/audit/audit.proto index c44c193c30b..cd449c1b246 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/audit/audit.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/audit/audit.proto @@ -22,11 +22,11 @@ service AuditApi { } message ListAuditRecordsRequest { - // Yamcs instance. + // Yamcs instance name optional string instance = 1; // The maximum number of returned records per page. Choose this value too high - // and you risk hitting the maximum response size limit enforced by the server. + // and you risk hitting the maximum response size limit enforced by the server. // Default: ``100`` optional int32 limit = 2; @@ -37,7 +37,7 @@ message ListAuditRecordsRequest { // ISO 8601 format. This bound is inclusive. optional google.protobuf.Timestamp start = 4; - // Filter the upper bound of the record's generation time. Specify a date string in + // Filter the upper bound of the record's time. Specify a date string in // ISO 8601 format. This bound is exclusive. optional google.protobuf.Timestamp stop = 5; @@ -49,6 +49,7 @@ message ListAuditRecordsRequest { } message ListAuditRecordsResponse { + // Page with matching records repeated AuditRecord records = 1; // Token indicating the response is only partial. More results can then diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/events/events_service.proto b/yamcs-api/src/main/proto/yamcs/protobuf/events/events_service.proto index a62e29113fe..01a9822ef90 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/events/events_service.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/events/events_service.proto @@ -19,6 +19,10 @@ service EventsApi { rpc ListEvents(ListEventsRequest) returns (ListEventsResponse) { option (yamcs.api.route) = { get: "/api/archive/{instance}/events" + additional_bindings { + post: "/api/archive/{instance}/events:list" + body: "*" + } }; } @@ -53,7 +57,7 @@ service EventsApi { } // Receive event updates - rpc SubscribeEvents(SubscribeEventsRequest) returns (stream Event) { + rpc SubscribeEvents(stream SubscribeEventsRequest) returns (stream Event) { option (yamcs.api.websocket) = { topic: "events" }; @@ -72,7 +76,7 @@ message ListEventsRequest { optional int64 pos = 2 [deprecated = true]; // The maximum number of returned records per page. Choose this value too high - // and you risk hitting the maximum response size limit enforced by the server. + // and you risk hitting the maximum response size limit enforced by the server. // Default: ``100`` optional int32 limit = 3; @@ -100,13 +104,47 @@ message ListEventsRequest { // Text to search for in the message. optional string q = 10; + + // Filter query. See :doc:`../filtering` for how to write a filter query. + // + // Literal text search matches against the fields ``message``, ``source`` and + // ``type``. + // + // Field comparisons can use any of the following fields: + // + // .. list-table:: + // :widths: 25 25 50 + // + // * - ``message`` + // - string + // - + // * - ``seqNumber`` + // - number + // - + // * - ``severity`` + // - enum + // - One of ``info``, ``watch``, ``warning``, ``distress``, ``critical`` or ``severe``. + // * - ``source`` + // - string + // - + // * - ``type`` + // - string + // - + optional string filter = 11; + + // If true, no events are returned. This option is currently used by + // yamcs-web to test the compilation of filter queries without hitting the + // Yamcs DB. + // + // Default: false + optional bool dryRun = 12; } message ListEventsResponse { // Deprecated, use ``events`` instead repeated Event event = 1 [deprecated=true]; - // Page of matching events + // Page with matching events repeated Event events = 3; // Token indicating the response is only partial. More results can then @@ -118,6 +156,33 @@ message ListEventsResponse { message SubscribeEventsRequest { // Yamcs instance name optional string instance = 1; + + // Filter query. See :doc:`../filtering` for how to write a filter query. + // + // Literal text search matches against the fields ``message``, ``source`` and + // ``type``. + // + // Field comparisons can use any of the following fields: + // + // .. list-table:: + // :widths: 25 25 50 + // + // * - ``message`` + // - string + // - + // * - ``seqNumber`` + // - number + // - + // * - ``severity`` + // - enum + // - One of ``info``, ``watch``, ``warning``, ``distress``, ``critical`` or ``severe``. + // * - ``source`` + // - string + // - + // * - ``type`` + // - string + // - + optional string filter = 2; } message CreateEventRequest { @@ -171,6 +236,33 @@ message StreamEventsRequest { // Search by text optional string q = 6; + + // Filter query. See :doc:`../filtering` for how to write a filter query. + // + // Literal text search matches against the fields ``message``, ``source`` and + // ``type``. + // + // Field comparisons can use any of the following fields: + // + // .. list-table:: + // :widths: 25 25 50 + // + // * - ``message`` + // - string + // - + // * - ``seqNumber`` + // - number + // - + // * - ``severity`` + // - enum + // - One of ``info``, ``watch``, ``warning``, ``distress``, ``critical`` or ``severe``. + // * - ``source`` + // - string + // - + // * - ``type`` + // - string + // - + optional string filter = 7; } message ListEventSourcesRequest { @@ -208,6 +300,33 @@ message ExportEventsRequest { // Text to search for in the message. optional string q = 6; + // Filter query. See :doc:`../filtering` for how to write a filter query. + // + // Literal text search matches against the fields ``message``, ``source`` and + // ``type``. + // + // Field comparisons can use any of the following fields: + // + // .. list-table:: + // :widths: 25 25 50 + // + // * - ``message`` + // - string + // - + // * - ``seqNumber`` + // - number + // - + // * - ``severity`` + // - enum + // - One of ``info``, ``watch``, ``warning``, ``distress``, ``critical`` or ``severe``. + // * - ``source`` + // - string + // - + // * - ``type`` + // - string + // - + optional string filter = 8; + // Column delimiter. One of ``TAB``, ``COMMA`` or ``SEMICOLON``. // Default: ``TAB``. optional string delimiter = 7; diff --git a/yamcs-api/src/main/proto/yamcs/protobuf/timeline/timeline.proto b/yamcs-api/src/main/proto/yamcs/protobuf/timeline/timeline.proto index 6af1538e351..9488de2a360 100644 --- a/yamcs-api/src/main/proto/yamcs/protobuf/timeline/timeline.proto +++ b/yamcs-api/src/main/proto/yamcs/protobuf/timeline/timeline.proto @@ -92,7 +92,7 @@ service TimelineApi { }; } - // Add a band. + // Add a band rpc AddBand(AddBandRequest) returns (TimelineBand) { option (yamcs.api.route) = { post: "/api/timeline/{instance}/bands" @@ -218,9 +218,12 @@ message TimelineItem { // If this item is part of a group, this is the group identifier optional string groupId = 7; - //if this item time specification is relative to another item, relativeTime contains a reference - // to that item as well as the relative start (the duration is the same as given by the duration above) - //note that start of the item will be computed by the server based on the relativeTime before sending the item to the client + // If this item time specification is relative to another item, ``relativeTime`` + // contains a reference to that item as well as the relative start (the duration + // is the same as the ``duration`` field). + // + // Note that start of the item is computed by the server based on the + // ``relativeTime`` before sending the item to the client. optional RelativeTime relativeTime = 8; // Item description @@ -278,18 +281,18 @@ message TimelineBand { // User who has created the band optional string username = 4; - //if true, all users have access to this band, otherwise only the user who has created it + // If true, all users have access to this band, otherwise only the user who has created it optional bool shared = 5; - //the band contains only items from this source + // The band contains only items from this source optional string source = 6; //the band contains only items with these tags; if the list is empty, then all items from the given source are part of the band //this is deprecated, the filters below should be used to select the items repeated string tags = 7 [deprecated=true]; - //The filters are to be considered in an AND manner. - // An item will be part of the band if it matches all the filters. + // The filters are to be considered in an AND manner. + // An item is part of the band if it matches all filters. // If the list is empty, then all items from the given source are part of the band repeated ItemFilter filters = 8; @@ -349,7 +352,7 @@ message CreateItemRequest { // If the item time specification is relative to another item, // ``relativeTime`` contains a reference to that item, as well // as the relative start (the duration is the same as given by the - // duration above). + // ``duration`` field). optional RelativeTime relativeTime = 9; // Item description @@ -387,27 +390,26 @@ message UpdateItemRequest { // Item name optional string name = 4; - //new start time + // New start time optional google.protobuf.Timestamp start = 5; - //new duration + // New duration optional google.protobuf.Duration duration = 6; - //new tags + // New tags repeated string tags = 7; // Set this to true to remove completely all the tags - // (required because in the protobuf we cannot distinguish between an empty tags list and a non existent list) optional bool clearTags = 8; - //new group identifier. - //to keep the old value, leave out - // to clear the group, set to an empty string + // New group identifier. + // + // To keep the old value, leave out. To clear the group, set to an empty string optional string groupId = 9; - //new relative time - // to keep the old value, leave out the property - // to clear, set the start + // New relative time. + // + // To keep the old value, leave out the property. To clear, set ``start``. optional RelativeTime relativeTime = 10; //new status (valid for activities) @@ -451,7 +453,7 @@ message ListItemsRequest { message DeleteItemRequest { - // Yamcs instance name. + // Yamcs instance name optional string instance = 1; // Item source @@ -488,7 +490,7 @@ message AddItemLogRequest { } message DeleteTimelineGroupRequest { - // Yamcs instance name. + // Yamcs instance name optional string instance = 1; // Item source @@ -556,7 +558,7 @@ message AddBandRequest { // this is deprecated, the filters below should be used instead repeated string tags = 5 [deprecated = true]; - //a query filter which can be used to limit additionally the items which are part of the band + // A query filter which can be used to limit additionally the items which are part of the band repeated ItemFilter filters = 6; // Type of band @@ -590,7 +592,7 @@ message UpdateBandRequest { // Band description optional string description = 4; - //if true, all users have access to this band, otherwise only the user who has created it + // If true, all users have access to this band, otherwise only the user who has created it optional bool shared = 5; // Items containing these tags will be part of the timeline @@ -599,10 +601,10 @@ message UpdateBandRequest { // Additional properties used by yamcs-web to render this band map properties = 7; - //Where the items shown on this band come from + // Where the items shown on this band come from optional string source = 8; - //filters to apply when retrieving the items + // Filters to apply when retrieving items repeated ItemFilter filters = 9; } @@ -612,7 +614,7 @@ message ListBandsRequest { } message DeleteBandRequest { - // Yamcs instance name. + // Yamcs instance name optional string instance = 1; // Item identifier @@ -633,7 +635,7 @@ message AddViewRequest { // View description optional string description = 3; - // The bands from this view + // The bands belonging to this view repeated string bands = 4; } @@ -663,12 +665,12 @@ message UpdateViewRequest { // View description optional string description = 4; - // The bands from this view + // The bands belonging to this view repeated string bands = 5; } message DeleteViewRequest { - // Yamcs instance name. + // Yamcs instance name optional string instance = 1; // Item identifier @@ -679,16 +681,21 @@ message ListViewsResponse { repeated TimelineView views = 1; } -//an item matches the filter if it matches any of the criteria from the list. -//if the list is empty, the filter will not match +// An item matches the filter if it matches any of the criteria from the list. +// If the list is empty, the filter will not match. message ItemFilter { - // The filter criteria depends very much of the source which provides the item - // rdb source - // supported key is "tag" - // -> An item matches if the value proprty is among its tags - // commands source - // supported key is "cmdNamePattern" - // value is considered as a regexp and match against the cmdName from the cmdhist table + // Available filter criteria depend on the item's source. + // + // When the source is rdb: + // + // tag + // An item matches if the value property is among its tags. + // + // When the source is commands: + // + // cmdNamePattern: + // value is considered as a regexp and matched against the ``cmdName`` + // column from the ``cmdhist`` table. message FilterCriterion { optional string key = 1; optional string value = 2; diff --git a/yamcs-client/src/main/java/org/yamcs/client/InstanceFilter.java b/yamcs-client/src/main/java/org/yamcs/client/InstanceFilter.java index 1866edafcf2..6ba26a85ddc 100644 --- a/yamcs-client/src/main/java/org/yamcs/client/InstanceFilter.java +++ b/yamcs-client/src/main/java/org/yamcs/client/InstanceFilter.java @@ -10,7 +10,7 @@ public class InstanceFilter { private List filterExpressions = new ArrayList<>(); public void addLabel(String label, String value) { - filterExpressions.add("label:" + label + "=" + value); + filterExpressions.add("label." + label + "=" + value); } public void setState(InstanceState state) { diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/EventFilter.java b/yamcs-core/src/main/java/org/yamcs/http/api/EventFilter.java new file mode 100644 index 00000000000..98b6b14dcf6 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/http/api/EventFilter.java @@ -0,0 +1,66 @@ +package org.yamcs.http.api; + +import static org.yamcs.StandardTupleDefinitions.BODY_COLUMN; +import static org.yamcs.StandardTupleDefinitions.SEQNUM_COLUMN; +import static org.yamcs.StandardTupleDefinitions.SOURCE_COLUMN; + +import org.yamcs.protobuf.Event.EventSeverity; +import org.yamcs.utils.parser.Filter; +import org.yamcs.utils.parser.ParseException; +import org.yamcs.utils.parser.UnknownFieldException; +import org.yamcs.yarch.Tuple; +import org.yamcs.yarch.protobuf.Db; + +public class EventFilter extends Filter { + + public EventFilter(String query) throws ParseException, UnknownFieldException { + super(query); + addEnumField("severity", EventSeverity.class, this::getSeverity); + addStringField("message", this::getMessage); + addStringField("source", this::getSource); + addStringField("type", this::getType); + addNumberField("seqNumber", this::getSequenceNumber); + parse(); + } + + private String getMessage(Tuple tuple) { + var event = (Db.Event) tuple.getColumn(BODY_COLUMN); + return event.hasMessage() ? event.getMessage() : null; + } + + private String getSource(Tuple tuple) { + return tuple.getColumn(SOURCE_COLUMN); + } + + private String getType(Tuple tuple) { + var event = (Db.Event) tuple.getColumn(BODY_COLUMN); + return event.hasType() ? event.getType() : null; + } + + private Number getSequenceNumber(Tuple tuple) { + return tuple.getColumn(SEQNUM_COLUMN); + } + + private EventSeverity getSeverity(Tuple tuple) { + var event = (Db.Event) tuple.getColumn(BODY_COLUMN); + return event.hasSeverity() ? event.getSeverity() : null; + } + + @Override + protected boolean matchesLiteral(Tuple tuple, String literal) { + var event = (Db.Event) tuple.getColumn(BODY_COLUMN); + if (event.getMessage().toLowerCase().contains(literal)) { + return true; + } + + if (event.getSource().toLowerCase().contains(literal)) { + return true; + } + + if (event.getType().toLowerCase().contains(literal)) { + return false; + } + + return false; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/EventFilterFactory.java b/yamcs-core/src/main/java/org/yamcs/http/api/EventFilterFactory.java new file mode 100644 index 00000000000..fee3abd0580 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/http/api/EventFilterFactory.java @@ -0,0 +1,62 @@ +package org.yamcs.http.api; + +import org.yamcs.api.FilterSyntaxException; +import org.yamcs.http.BadRequestException; +import org.yamcs.utils.parser.IncorrectTypeException; +import org.yamcs.utils.parser.ParseException; +import org.yamcs.utils.parser.TokenMgrError; +import org.yamcs.utils.parser.UnknownFieldException; + +public class EventFilterFactory { + + public static EventFilter create(String query) { + try { + return new EventFilter(query); + } catch (UnknownFieldException | IncorrectTypeException e) { + throw mapCustomParseException(e); + } catch (ParseException e) { + throw mapParseException(e); + } catch (TokenMgrError e) { + throw mapTokenMgrError(e); + } + } + + private static BadRequestException mapCustomParseException(ParseException e) { + var exc = new BadRequestException(e.getMessage()); + if (e.currentToken != null) { + exc.setDetail(FilterSyntaxException.newBuilder() + .setBeginLine(e.currentToken.beginLine) + .setBeginColumn(e.currentToken.beginColumn) + .setEndLine(e.currentToken.endLine) + .setEndColumn(e.currentToken.endColumn) + .build()); + } + throw exc; + } + + private static BadRequestException mapParseException(ParseException e) { + var exc = new BadRequestException("Syntax error in filter"); + if (e.currentToken != null) { + exc.setDetail(FilterSyntaxException.newBuilder() + .setBeginLine(e.currentToken.beginLine) + .setBeginColumn(e.currentToken.beginColumn) + .setEndLine(e.currentToken.endLine) + .setEndColumn(e.currentToken.endColumn) + .build()); + } + throw exc; + } + + private static BadRequestException mapTokenMgrError(TokenMgrError e) { + var exc = new BadRequestException("Syntax error in filter"); + if (e.errorLine >= 0 && e.errorColumn >= 0) { + exc.setDetail(FilterSyntaxException.newBuilder() + .setBeginLine(e.errorLine) + .setBeginColumn(e.errorColumn) + .setEndLine(e.errorLine) + .setEndColumn(e.errorColumn) + .build()); + } + throw exc; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java b/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java index 34299e8969a..d3dbd496ea9 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java +++ b/yamcs-core/src/main/java/org/yamcs/http/api/EventsApi.java @@ -126,33 +126,45 @@ public void listEvents(Context ctx, ListEventsRequest request, Observer limit) { - EventPageToken token = new EventPageToken(last.getGenerationTime(), last.getSource(), - last.getSeqNumber()); - responseb.setContinuationToken(token.encodeAsString()); + @Override + public void streamClosed(Stream stream) { + if (count > limit) { + var token = new EventPageToken(last.getGenerationTime(), last.getSource(), + last.getSeqNumber()); + responseb.setContinuationToken(token.encodeAsString()); + } + observer.complete(responseb.build()); } - observer.complete(responseb.build()); - } - }); + }); + } } @Override @@ -216,7 +228,7 @@ public void createEvent(Context ctx, CreateEventRequest request, Observer // Distribute event (without augmented fields, or they'll get stored) Db.Event event = eventb.build(); - log.debug("Adding event: {}", event.toString()); + log.debug("Adding event: {}", event); eventProducer.sendEvent(event); // Send back the event in response @@ -248,29 +260,11 @@ public void listEventSources(Context ctx, ListEventSourcesRequest request, } @Override - public void subscribeEvents(Context ctx, SubscribeEventsRequest request, Observer observer) { - String instance = InstancesApi.verifyInstance(request.getInstance()); + public Observer subscribeEvents(Context ctx, Observer observer) { ctx.checkSystemPrivilege(SystemPrivilege.ReadEvents); - YarchDatabaseInstance ydb = YarchDatabase.getInstance(instance); - Stream stream = ydb.getStream(EventRecorder.REALTIME_EVENT_STREAM_NAME); - if (stream == null) { - return; // No error, just don't send data - } - - StreamSubscriber listener = new StreamSubscriber() { - @Override - public void onTuple(Stream stream, Tuple tuple) { - Db.Event event = (Db.Event) tuple.getColumn("body"); - observer.next(fromDbEvent(event)); - } - - @Override - public void streamClosed(Stream stream) { - observer.complete(); - } - }; - observer.setCancelHandler(() -> stream.removeSubscriber(listener)); - stream.addSubscriber(listener); + var clientObserver = new SubscribeEventsObserver(observer); + observer.setCancelHandler(() -> clientObserver.complete()); + return clientObserver; } @Override @@ -298,10 +292,18 @@ public void streamEvents(Context ctx, StreamEventsRequest request, Observer observer; + EventFilter filter; char columnDelimiter; - CsvEventStreamer(Observer observer, char columnDelimiter) { + CsvEventStreamer(Observer observer, EventFilter filter, char columnDelimiter) { this.observer = observer; + this.filter = filter; this.columnDelimiter = columnDelimiter; String[] rec = new String[5]; @@ -444,6 +452,10 @@ public void onTuple(Stream stream, Tuple tuple) { return; } + if (filter != null && !filter.matches(tuple)) { + return; + } + Db.Event incoming = (Db.Event) tuple.getColumn("body"); Event event = fromDbEvent(incoming); diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/InstancesApi.java b/yamcs-core/src/main/java/org/yamcs/http/api/InstancesApi.java index f63eb8a0a28..f2aa465f213 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/api/InstancesApi.java +++ b/yamcs-core/src/main/java/org/yamcs/http/api/InstancesApi.java @@ -29,6 +29,7 @@ import org.yamcs.logging.Log; import org.yamcs.management.ManagementListener; import org.yamcs.management.ManagementService; +import org.yamcs.mdb.Mdb; import org.yamcs.plists.ParameterListService; import org.yamcs.protobuf.AbstractInstancesApi; import org.yamcs.protobuf.CreateInstanceRequest; @@ -53,10 +54,9 @@ import org.yamcs.utils.ExceptionUtil; import org.yamcs.utils.TimeEncoding; import org.yamcs.utils.parser.FilterParser; -import org.yamcs.utils.parser.FilterParser.Result; import org.yamcs.utils.parser.ParseException; import org.yamcs.utils.parser.TokenMgrError; -import org.yamcs.mdb.Mdb; +import org.yamcs.utils.parser.ast.Comparison; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.protobuf.Empty; @@ -295,14 +295,24 @@ private Predicate getFilter(List flist) throws Http return ysi -> true; } - FilterParser fp = new FilterParser((StringReader) null); + var fp = new FilterParser((StringReader) null); + fp.addEnumField("state", InstanceState.class, ysi -> ysi.state()); + fp.addPrefixField("label.", (ysi, field) -> { + var label = field.substring("label.".length()); + return ysi.getLabels().get(label); + }); Predicate pred = ysi -> true; for (String filter : flist) { + // Temporary backwards support for an (undocumented) API change. + // Can be removed in a few months. + if (filter.startsWith("label:")) { + filter = filter.replace("label:", "label."); + } fp.ReInit(new StringReader(filter)); - FilterParser.Result pr; + Comparison pr; try { - pr = fp.parse(); + pr = fp.comparison(); pred = pred.and(getPredicate(pr)); } catch (ParseException | TokenMgrError e) { throw new BadRequestException("Cannot parse the filter '" + filter + "': " + e.getMessage()); @@ -312,24 +322,24 @@ private Predicate getFilter(List flist) throws Http return pred; } - private Predicate getPredicate(Result pr) throws HttpException { - if ("state".equals(pr.key)) { + private Predicate getPredicate(Comparison pr) throws HttpException { + if ("state".equals(pr.comparable)) { try { InstanceState state = InstanceState.valueOf(pr.value.toUpperCase()); - switch (pr.op) { - case EQUAL: + switch (pr.comparator) { + case EQUAL_TO: return ysi -> ysi.state() == state; - case NOT_EQUAL: + case NOT_EQUAL_TO: return ysi -> ysi.state() != state; default: - throw new IllegalStateException("Unknown operator " + pr.op); + throw new IllegalStateException("Unknown operator " + pr.comparator); } } catch (IllegalArgumentException e) { throw new BadRequestException( "Unknown state '" + pr.value + "'. Valid values are: " + Arrays.asList(InstanceState.values())); } - } else if (pr.key.startsWith("label:")) { - String labelKey = pr.key.substring(6); + } else if (pr.comparable.startsWith("label.")) { + String labelKey = pr.comparable.substring(6); return ysi -> { Map labels = ysi.getLabels(); if (labels == null) { @@ -339,17 +349,17 @@ private Predicate getPredicate(Result pr) throws HttpExcept if (o == null) { return false; } - switch (pr.op) { - case EQUAL: + switch (pr.comparator) { + case EQUAL_TO: return pr.value.equals(o); - case NOT_EQUAL: + case NOT_EQUAL_TO: return !pr.value.equals(o); default: - throw new IllegalStateException("Unknown operator " + pr.op); + throw new IllegalStateException("Unknown operator " + pr.comparator); } }; } else { - throw new BadRequestException("Unknown filter key '" + pr.key + "'"); + throw new BadRequestException("Unknown filter key '" + pr.comparable + "'"); } } diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/ParameterListsApi.java b/yamcs-core/src/main/java/org/yamcs/http/api/ParameterListsApi.java index c8e1d623458..256c9e0bec1 100644 --- a/yamcs-core/src/main/java/org/yamcs/http/api/ParameterListsApi.java +++ b/yamcs-core/src/main/java/org/yamcs/http/api/ParameterListsApi.java @@ -222,7 +222,7 @@ private static UUID verifyId(String id) { try { return UUID.fromString(id); } catch (IllegalArgumentException e) { - throw new BadRequestException("Invalid activity identifier '" + id + "'"); + throw new BadRequestException("Invalid identifier '" + id + "'"); } } } diff --git a/yamcs-core/src/main/java/org/yamcs/http/api/SubscribeEventsObserver.java b/yamcs-core/src/main/java/org/yamcs/http/api/SubscribeEventsObserver.java new file mode 100644 index 00000000000..a955359b8ce --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/http/api/SubscribeEventsObserver.java @@ -0,0 +1,76 @@ +package org.yamcs.http.api; + +import static org.yamcs.StandardTupleDefinitions.BODY_COLUMN; + +import org.yamcs.api.Observer; +import org.yamcs.archive.EventRecorder; +import org.yamcs.protobuf.Event; +import org.yamcs.protobuf.SubscribeEventsRequest; +import org.yamcs.yarch.Stream; +import org.yamcs.yarch.StreamSubscriber; +import org.yamcs.yarch.Tuple; +import org.yamcs.yarch.YarchDatabase; +import org.yamcs.yarch.protobuf.Db; + +public class SubscribeEventsObserver implements Observer, StreamSubscriber { + + private Observer responseObserver; + + private Stream stream; + + private EventFilter filter; + + public SubscribeEventsObserver(Observer responseObserver) { + this.responseObserver = responseObserver; + } + + @Override + public void next(SubscribeEventsRequest request) { + if (stream != null) { + stream.removeSubscriber(this); + stream = null; + } + + filter = request.hasFilter() + ? EventFilterFactory.create(request.getFilter()) + : null; + + var instance = InstancesApi.verifyInstance(request.getInstance()); + var ydb = YarchDatabase.getInstance(instance); + stream = ydb.getStream(EventRecorder.REALTIME_EVENT_STREAM_NAME); + if (stream == null) { + return; // No error, just don't send data + } + + stream.addSubscriber(this); + } + + @Override + public void onTuple(Stream stream, Tuple tuple) { + if (filter != null && !filter.matches(tuple)) { + return; + } + + var event = (Db.Event) tuple.getColumn(BODY_COLUMN); + responseObserver.next(EventsApi.fromDbEvent(event)); + } + + @Override + public void streamClosed(Stream stream) { + // Ignore + } + + @Override + public void completeExceptionally(Throwable t) { + if (stream != null) { + stream.removeSubscriber(this); + } + } + + @Override + public void complete() { + if (stream != null) { + stream.removeSubscriber(this); + } + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/Filter.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/Filter.java new file mode 100644 index 00000000000..d6108c3e191 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/Filter.java @@ -0,0 +1,353 @@ +package org.yamcs.utils.parser; + +import java.io.StringReader; +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.yamcs.utils.parser.ast.AndExpression; +import org.yamcs.utils.parser.ast.Comparison; +import org.yamcs.utils.parser.ast.OrExpression; +import org.yamcs.utils.parser.ast.UnaryExpression; + +import com.google.common.primitives.Bytes; + +public abstract class Filter { + + private FilterParser parser; + private AndExpression expression; + + public Filter(String query) { + parser = new FilterParser<>(new StringReader(query)); + } + + public void parse() throws ParseException { + expression = parser.parse(); + } + + protected void addPrefixField(String field, BiFunction resolver) { + parser.addPrefixField(field, resolver); + } + + protected void addStringField(String field, Function resolver) { + parser.addStringField(field, resolver); + } + + protected > void addEnumField(String field, Class enumClass, Function resolver) { + parser.addEnumField(field, enumClass, resolver); + } + + protected void addNumberField(String field, Function resolver) { + parser.addNumberField(field, resolver); + } + + protected void addBooleanField(String field, Function resolver) { + parser.addBooleanField(field, resolver); + } + + protected void addBinaryField(String field, Function resolver) { + parser.addBinaryField(field, resolver); + } + + public boolean matches(T item) { + if (expression == null) { + return true; + } else { + return matchAndExpression(expression, item); + } + } + + public String printExpression() { + return expression.toString(" "); + } + + protected abstract boolean matchesLiteral(T item, String literal); + + private boolean matchOrExpression(OrExpression expression, T item) { + for (UnaryExpression clause : expression.getClauses()) { + if (matchUnaryExpression(clause, item)) { + return true; + } + } + return false; + } + + private boolean matchAndExpression(AndExpression expression, T item) { + for (OrExpression clause : expression.getClauses()) { + if (!matchOrExpression(clause, item)) { + return false; + } + } + return true; + } + + private boolean matchUnaryExpression(UnaryExpression expression, T item) { + boolean res; + if (expression.getComparison() != null) { + res = matchComparison(expression.getComparison(), item); + } else { + res = matchAndExpression(expression.getAndExpression(), item); + } + return expression.isNot() ? !res : res; + } + + private boolean matchComparison(Comparison comparison, T item) { + if (comparison.comparator == null) { + return matchesLiteral(item, comparison.comparable.toLowerCase()); + } + + var stringResolver = parser.getStringResolver(comparison.comparable); + if (stringResolver != null) { + var fieldValue = stringResolver.apply(item); + return matchStringComparison(comparison, fieldValue); + } + + var enumResolver = parser.getEnumResolver(comparison.comparable); + if (enumResolver != null) { + return matchEnumComparison(comparison, item, enumResolver); + } + + var numberResolver = parser.getNumberResolver(comparison.comparable); + if (numberResolver != null) { + return matchNumberComparison(comparison, item, numberResolver); + } + + var booleanResolver = parser.getBooleanResolver(comparison.comparable); + if (booleanResolver != null) { + return matchBooleanComparison(comparison, item, booleanResolver); + } + + var binaryResolver = parser.getBinaryResolver(comparison.comparable); + if (binaryResolver != null) { + return matchBinaryComparison(comparison, item, binaryResolver); + } + + var prefixResolver = parser.getPrefixResolver(comparison.comparable); + if (prefixResolver != null) { + var fieldValue = prefixResolver.apply(item, comparison.comparable); + return matchStringComparison(comparison, fieldValue); + } + + throw new IllegalArgumentException("Unexpected field '" + comparison.comparable + "'"); + } + + private boolean matchStringComparison(Comparison comparison, String fieldValue) { + switch (comparison.comparator) { + case EQUAL_TO: + return isEqual(fieldValue, comparison.value); // Faster than compare + case NOT_EQUAL_TO: + return !isEqual(fieldValue, comparison.value); // Faster than compare + case GREATER_THAN: + return compareStringField(fieldValue, comparison.value) > 0; + case GREATER_THAN_OR_EQUAL_TO: + return compareStringField(fieldValue, comparison.value) >= 0; + case LESS_THAN: + return compareStringField(fieldValue, comparison.value) < 0; + case LESS_THAN_OR_EQUAL_TO: + return compareStringField(fieldValue, comparison.value) <= 0; + case HAS: + return testStringFieldContains(fieldValue, comparison.value); + case RE_EQUAL_TO: + return testRegexMatch(fieldValue, comparison.pattern); + case RE_NOT_EQUAL_TO: + return !testRegexMatch(fieldValue, comparison.pattern); + default: + throw new IllegalStateException("Unexpected comparator " + comparison.comparator); + } + } + + private boolean matchEnumComparison(Comparison comparison, T item, Function> resolver) { + Class> enumClass = parser.getEnumClass(comparison.comparable); + Enum comparand = null; + if (enumClass != null) { + comparand = parser.findEnum(enumClass, comparison.value); + } + + var fieldValue = resolver.apply(item); + switch (comparison.comparator) { + case EQUAL_TO: + case HAS: + case RE_EQUAL_TO: + return compareEnumField(fieldValue, comparand) == 0; + case NOT_EQUAL_TO: + case RE_NOT_EQUAL_TO: + return compareEnumField(fieldValue, comparand) != 0; + case GREATER_THAN: + return compareEnumField(fieldValue, comparand) > 0; + case GREATER_THAN_OR_EQUAL_TO: + return compareEnumField(fieldValue, comparand) >= 0; + case LESS_THAN: + return compareEnumField(fieldValue, comparand) < 0; + case LESS_THAN_OR_EQUAL_TO: + return compareEnumField(fieldValue, comparand) <= 0; + default: + throw new IllegalStateException("Unexpected comparator " + comparison.comparator); + } + } + + private boolean matchNumberComparison(Comparison comparison, T item, Function resolver) { + var fieldValue = resolver.apply(item); + var comparand = comparison.value.equalsIgnoreCase("null") + ? null + : Double.parseDouble(comparison.value); + + switch (comparison.comparator) { + case EQUAL_TO: + case HAS: + case RE_EQUAL_TO: + return compareNumberField(fieldValue, comparand) == 0; + case NOT_EQUAL_TO: + case RE_NOT_EQUAL_TO: + return compareNumberField(fieldValue, comparand) != 0; + case GREATER_THAN: + return compareNumberField(fieldValue, comparand) > 0; + case GREATER_THAN_OR_EQUAL_TO: + return compareNumberField(fieldValue, comparand) >= 0; + case LESS_THAN: + return compareNumberField(fieldValue, comparand) < 0; + case LESS_THAN_OR_EQUAL_TO: + return compareNumberField(fieldValue, comparand) <= 0; + default: + throw new IllegalStateException("Unexpected comparator " + comparison.comparator); + } + } + + private boolean matchBooleanComparison(Comparison comparison, T item, Function resolver) { + var fieldValue = resolver.apply(item); + var comparand = comparison.value.equalsIgnoreCase("null") + ? null + : Boolean.parseBoolean(comparison.value); + + switch (comparison.comparator) { + case EQUAL_TO: + case HAS: + case RE_EQUAL_TO: + return compareBooleanField(fieldValue, comparand) == 0; + case NOT_EQUAL_TO: + case RE_NOT_EQUAL_TO: + return compareBooleanField(fieldValue, comparand) != 0; + case GREATER_THAN: + return compareBooleanField(fieldValue, comparand) > 0; + case GREATER_THAN_OR_EQUAL_TO: + return compareBooleanField(fieldValue, comparand) >= 0; + case LESS_THAN: + return compareBooleanField(fieldValue, comparand) < 0; + case LESS_THAN_OR_EQUAL_TO: + return compareBooleanField(fieldValue, comparand) <= 0; + default: + throw new IllegalStateException("Unexpected comparator " + comparison.comparator); + } + } + + private boolean matchBinaryComparison(Comparison comparison, T item, Function resolver) { + var fieldValue = resolver.apply(item); + var comparand = comparison.binary; + + switch (comparison.comparator) { + case EQUAL_TO: + case RE_EQUAL_TO: + return Arrays.equals(fieldValue, comparand); + case HAS: + return Bytes.indexOf(fieldValue, comparand) != -1; + case NOT_EQUAL_TO: + case RE_NOT_EQUAL_TO: + return !Arrays.equals(fieldValue, comparand); + case GREATER_THAN: + return compareBinaryField(fieldValue, comparand) > 0; + case GREATER_THAN_OR_EQUAL_TO: + return compareBinaryField(fieldValue, comparand) >= 0; + case LESS_THAN: + return compareBinaryField(fieldValue, comparand) < 0; + case LESS_THAN_OR_EQUAL_TO: + return compareBinaryField(fieldValue, comparand) <= 0; + default: + throw new IllegalStateException("Unexpected comparator " + comparison.comparator); + } + } + + private boolean isEqual(String fieldValue, String comparand) { + if (fieldValue == null) { + return comparand.equalsIgnoreCase("null"); + } + return fieldValue.equalsIgnoreCase(comparand); + } + + private boolean testRegexMatch(String fieldValue, Pattern comparand) { + if (fieldValue == null) { + return false; + } + // Unanchored regex + return comparand.matcher(fieldValue).find(); + } + + private int compareStringField(String fieldValue, String comparand) { + if (fieldValue == null) { + return -1; + } + return fieldValue.compareToIgnoreCase(comparand); + } + + private boolean testStringFieldContains(String fieldValue, String comparand) { + if (fieldValue == null) { + return false; + } + return fieldValue.toLowerCase().contains(comparand); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private int compareEnumField(Enum fieldValue, Enum comparand) { + // Nulls first + if (fieldValue == null) { + return comparand == null ? 0 : -1; + } else if (comparand == null) { + return 1; + } + return fieldValue.compareTo(comparand); + } + + private int compareNumberField(Number fieldValue, Double comparand) { + // Nulls first + if (fieldValue == null) { + return comparand == null ? 0 : -1; + } else if (comparand == null) { + return 1; + } + + if (fieldValue instanceof Integer i) { + return Double.compare(i, comparand); + } else if (fieldValue instanceof Long l) { + return Double.compare(l, comparand); + } else if (fieldValue instanceof Double d) { + return Double.compare(d, comparand); + } else if (fieldValue instanceof Float f) { + return Double.compare(f, comparand); + } else if (fieldValue instanceof Short s) { + return Double.compare(s, comparand); + } else if (fieldValue instanceof Byte b) { + return Double.compare(b, comparand); + } else { + throw new IllegalArgumentException("Unexpected number class"); + } + } + + private int compareBooleanField(Boolean fieldValue, Boolean comparand) { + // Nulls first + if (fieldValue == null) { + return comparand == null ? 0 : -1; + } else if (comparand == null) { + return 1; + } + return fieldValue.compareTo(comparand); + } + + private int compareBinaryField(byte[] fieldValue, byte[] comparand) { + // Nulls first + if (fieldValue == null) { + return comparand == null ? 0 : -1; + } else if (comparand == null) { + return 1; + } + return Arrays.compare(fieldValue, comparand); + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.java index 6b4c62e60be..ccffda77e58 100644 --- a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.java +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.java @@ -1,47 +1,354 @@ /* Generated By:JavaCC: Do not edit this line. FilterParser.java */ package org.yamcs.utils.parser; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.yamcs.utils.parser.ast.*; -/** ID lister. */ -public class FilterParser implements FilterParserConstants { +@SuppressWarnings({"serial", "unused"}) +public class FilterParser implements FilterParserConstants { - public static enum Operator { - EQUAL, NOT_EQUAL; + private static final HexFormat HEX = HexFormat.of(); + + private Set fields = new HashSet(); + + private Map> prefixResolvers = new HashMap>(); + private Map> stringResolvers = new HashMap>(); + private Map> numberResolvers = new HashMap>(); + private Map> booleanResolvers = new HashMap>(); + private Map> binaryResolvers = new HashMap>(); + private Map>> enumResolvers = new HashMap>>(); + + private Map>> enumClassByField = new HashMap>>(); + + public void addPrefixField(String field, BiFunction resolver) { + fields.add(field); + prefixResolvers.put(field, resolver); + } + + public void addStringField(String field, Function resolver) { + fields.add(field); + stringResolvers.put(field, resolver); + } + + public > void addEnumField(String field, Class enumClass, Function resolver) { + fields.add(field); + enumResolvers.put(field, resolver); + enumClassByField.put(field, enumClass); + } + + public void addNumberField(String field, Function resolver) { + fields.add(field); + numberResolvers.put(field, resolver); + } + + public void addBooleanField(String field, Function resolver) { + fields.add(field); + booleanResolvers.put(field, resolver); + } + + public void addBinaryField(String field, Function resolver) { + fields.add(field); + binaryResolvers.put(field, resolver); + } + + public BiFunction getPrefixResolver(String field) { + for (var entry : prefixResolvers.entrySet()) { + if (field.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + + public Function getStringResolver(String field) { + return stringResolvers.get(field); + } + + public Function getNumberResolver(String field) { + return numberResolvers.get(field); + } + + public Function getBooleanResolver(String field) { + return booleanResolvers.get(field); + } + + public Function getBinaryResolver(String field) { + return binaryResolvers.get(field); + } + + public Function> getEnumResolver(String field) { + return enumResolvers.get(field); + } + + public Class> getEnumClass(String field) { + return enumClassByField.get(field); + } + + /** + * Finds the constant for an Enum label, but case-insensitive. + */ + public > E findEnum(Class enumeration, String value) { + for (E enumConstant : enumeration.getEnumConstants()) { + if (enumConstant.name().compareToIgnoreCase(value) == 0) { + return enumConstant; + } + } + return null; + } + + final public AndExpression parse() throws ParseException { + AndExpression result = null; + if (jj_2_1(2)) { + jj_consume_token(WS); + } else { + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case LPAREN: + case MINUS: + case NOT: + case STRING: + case QUOTED_STRING: + result = expr(); + break; + default: + jj_la1[0] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[1] = jj_gen; + ; + } + jj_consume_token(0); + {if (true) return result;} + throw new Error("Missing return statement in function"); } - public static class Result { - public final String key; - public final Operator op; - public final String value; + final public AndExpression expr() throws ParseException { + AndExpression and; + and = and(); + if (jj_2_2(2)) { + jj_consume_token(WS); + } else { + ; + } + {if (true) return and;} + throw new Error("Missing return statement in function"); + } - public Result(String key, Operator op, String value) { - this.key = key; - this.op = op; - this.value = value; + final public AndExpression and() throws ParseException { + OrExpression clause; + List clauses = new ArrayList(); + clause = or(); + clauses.add(clause); + label_1: + while (true) { + if (jj_2_3(2)) { + ; + } else { + break label_1; } + jj_consume_token(WS); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + jj_consume_token(AND); + jj_consume_token(WS); + break; + default: + jj_la1[2] = jj_gen; + ; + } + clause = or(); + clauses.add(clause); + } + {if (true) return new AndExpression(clauses);} + throw new Error("Missing return statement in function"); + } - @Override - public String toString() { - return "Result [key: '" + key + "', op: '" + op + "', value: '" + value + "']"; + final public OrExpression or() throws ParseException { + UnaryExpression clause; + List clauses = new ArrayList(); + clause = unary(); + clauses.add(clause); + label_2: + while (true) { + if (jj_2_4(2)) { + ; + } else { + break label_2; } + jj_consume_token(WS); + jj_consume_token(OR); + jj_consume_token(WS); + clause = unary(); + clauses.add(clause); + } + {if (true) return new OrExpression(clauses);} + throw new Error("Missing return statement in function"); } - final public Result parse() throws ParseException { - Result r; - r = expr(); - jj_consume_token(0); - {if (true) return r;} + final public UnaryExpression unary() throws ParseException { + Comparison comparison; + AndExpression expr; + if (jj_2_5(3)) { + jj_consume_token(NOT); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[3] = jj_gen; + ; + } + jj_consume_token(LPAREN); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[4] = jj_gen; + ; + } + expr = expr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[5] = jj_gen; + ; + } + jj_consume_token(RPAREN); + {if (true) return new UnaryExpression(expr, true);} + } else { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + jj_consume_token(NOT); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[6] = jj_gen; + ; + } + comparison = comparison(); + {if (true) return new UnaryExpression(comparison, true);} + break; + case MINUS: + jj_consume_token(MINUS); + comparison = comparison(); + {if (true) return new UnaryExpression(comparison, true);} + break; + case LPAREN: + jj_consume_token(LPAREN); + expr = expr(); + jj_consume_token(RPAREN); + {if (true) return new UnaryExpression(expr, false);} + break; + case STRING: + case QUOTED_STRING: + comparison = comparison(); + {if (true) return new UnaryExpression(comparison, false);} + break; + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } throw new Error("Missing return statement in function"); } -/** Top level production. */ - final public Result expr() throws ParseException { -String key, value; -Operator op; - key = term(); - op = op(); - value = term(); - {if (true) return new Result(key, op, value);} + final public Comparison comparison() throws ParseException { + String comparable; + Token comparableToken; + Token comparatorToken = null; + Comparator comparator = null; + String value = null; + Pattern pattern = null; + byte[] binary = null; + comparable = term(); + comparableToken = token; + if (jj_2_6(2)) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[8] = jj_gen; + ; + } + comparator = comparator(); + comparatorToken = token; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WS: + jj_consume_token(WS); + break; + default: + jj_la1[9] = jj_gen; + ; + } + value = term(); + } else { + ; + } + if (comparator != null) { + if (!fields.contains(comparable)) { + boolean prefixMatch = false; + for (String prefix : prefixResolvers.keySet()) { + if (comparable.startsWith(prefix)) { + prefixMatch = true; + break; + } + } + if (!prefixMatch) { + {if (true) throw new UnknownFieldException(comparable, comparableToken, tokenImage);} + } + } + + Class> enumClass = enumClassByField.get(comparable); + if (enumClass != null) { + if (!value.equalsIgnoreCase("null") && findEnum(enumClass, value) == null) { + {if (true) throw new IncorrectTypeException(value, token, tokenImage);} + } + } + + if (binaryResolvers.containsKey(comparable)) { + if (!value.equalsIgnoreCase("null")) { + try { + binary = HEX.parseHex(value); + } catch (NumberFormatException e) { + {if (true) throw new IncorrectTypeException(value, token, tokenImage);} + } + } + } + + if (comparator == Comparator.RE_EQUAL_TO || comparator == Comparator.RE_NOT_EQUAL_TO) { + try { + pattern = Pattern.compile(value); + } catch (PatternSyntaxException e) { + {if (true) throw new InvalidPatternException(value, token, tokenImage);} + } + } + } + + {if (true) return new Comparison(comparable, comparator, value, pattern, binary);} throw new Error("Missing return statement in function"); } @@ -49,38 +356,317 @@ final public String term() throws ParseException { switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { case STRING: jj_consume_token(STRING); - {if (true) return token.image;} + {if (true) return token.image;} break; case QUOTED_STRING: jj_consume_token(QUOTED_STRING); - String s = token.image; {if (true) return s.substring(1, s.length() - 1).replace("\u005c\u005c\u005c"","\u005c"").replace("\u005c\u005c\u005c\u005c","\u005c\u005c");} + String s = token.image; + {if (true) return s.substring(1, s.length() - 1).replace("\u005c\u005c\u005c"","\u005c"").replace("\u005c\u005c\u005c\u005c","\u005c\u005c");} break; default: - jj_la1[0] = jj_gen; + jj_la1[10] = jj_gen; jj_consume_token(-1); throw new ParseException(); } throw new Error("Missing return statement in function"); } - final public Operator op() throws ParseException { + final public Comparator comparator() throws ParseException { switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { - case EQUAL: - jj_consume_token(EQUAL); - {if (true) return Operator.EQUAL;} + case EQUAL_TO: + jj_consume_token(EQUAL_TO); + {if (true) return Comparator.EQUAL_TO;} + break; + case NOT_EQUAL_TO: + jj_consume_token(NOT_EQUAL_TO); + {if (true) return Comparator.NOT_EQUAL_TO;} + break; + case LESS_THAN: + jj_consume_token(LESS_THAN); + {if (true) return Comparator.LESS_THAN;} + break; + case GREATER_THAN: + jj_consume_token(GREATER_THAN); + {if (true) return Comparator.GREATER_THAN;} + break; + case LESS_THAN_OR_EQUAL_TO: + jj_consume_token(LESS_THAN_OR_EQUAL_TO); + {if (true) return Comparator.LESS_THAN_OR_EQUAL_TO;} + break; + case GREATER_THAN_OR_EQUAL_TO: + jj_consume_token(GREATER_THAN_OR_EQUAL_TO); + {if (true) return Comparator.GREATER_THAN_OR_EQUAL_TO;} + break; + case HAS: + jj_consume_token(HAS); + {if (true) return Comparator.HAS;} + break; + case RE_EQUAL_TO: + jj_consume_token(RE_EQUAL_TO); + {if (true) return Comparator.RE_EQUAL_TO;} break; - case NOT_EQUAL: - jj_consume_token(NOT_EQUAL); - {if (true) return Operator.NOT_EQUAL;} + case RE_NOT_EQUAL_TO: + jj_consume_token(RE_NOT_EQUAL_TO); + {if (true) return Comparator.RE_NOT_EQUAL_TO;} break; default: - jj_la1[1] = jj_gen; + jj_la1[11] = jj_gen; jj_consume_token(-1); throw new ParseException(); } throw new Error("Missing return statement in function"); } + private boolean jj_2_1(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_1(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + private boolean jj_2_2(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_2(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(1, xla); } + } + + private boolean jj_2_3(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_3(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(2, xla); } + } + + private boolean jj_2_4(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_4(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(3, xla); } + } + + private boolean jj_2_5(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_5(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(4, xla); } + } + + private boolean jj_2_6(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_6(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(5, xla); } + } + + private boolean jj_3_3() { + if (jj_scan_token(WS)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_3()) jj_scanpos = xsp; + if (jj_3R_4()) return true; + return false; + } + + private boolean jj_3_6() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(2)) jj_scanpos = xsp; + if (jj_3R_6()) return true; + xsp = jj_scanpos; + if (jj_scan_token(2)) jj_scanpos = xsp; + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_9() { + if (jj_3R_4()) return true; + return false; + } + + private boolean jj_3R_25() { + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_5() { + if (jj_3R_9()) return true; + return false; + } + + private boolean jj_3_1() { + if (jj_scan_token(WS)) return true; + return false; + } + + private boolean jj_3R_24() { + if (jj_3R_25()) return true; + return false; + } + + private boolean jj_3R_23() { + if (jj_scan_token(LPAREN)) return true; + return false; + } + + private boolean jj_3R_22() { + if (jj_scan_token(MINUS)) return true; + return false; + } + + private boolean jj_3R_21() { + if (jj_scan_token(NOT)) return true; + return false; + } + + private boolean jj_3_5() { + if (jj_scan_token(NOT)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(2)) jj_scanpos = xsp; + if (jj_scan_token(LPAREN)) return true; + xsp = jj_scanpos; + if (jj_scan_token(2)) jj_scanpos = xsp; + if (jj_3R_5()) return true; + return false; + } + + private boolean jj_3R_8() { + Token xsp; + xsp = jj_scanpos; + if (jj_3_5()) { + jj_scanpos = xsp; + if (jj_3R_21()) { + jj_scanpos = xsp; + if (jj_3R_22()) { + jj_scanpos = xsp; + if (jj_3R_23()) { + jj_scanpos = xsp; + if (jj_3R_24()) return true; + } + } + } + } + return false; + } + + private boolean jj_3R_18() { + if (jj_scan_token(RE_NOT_EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_17() { + if (jj_scan_token(RE_EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_3() { + if (jj_scan_token(AND)) return true; + return false; + } + + private boolean jj_3R_16() { + if (jj_scan_token(HAS)) return true; + return false; + } + + private boolean jj_3R_15() { + if (jj_scan_token(GREATER_THAN_OR_EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_14() { + if (jj_scan_token(LESS_THAN_OR_EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_13() { + if (jj_scan_token(GREATER_THAN)) return true; + return false; + } + + private boolean jj_3R_12() { + if (jj_scan_token(LESS_THAN)) return true; + return false; + } + + private boolean jj_3_4() { + if (jj_scan_token(WS)) return true; + if (jj_scan_token(OR)) return true; + return false; + } + + private boolean jj_3R_11() { + if (jj_scan_token(NOT_EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_6() { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_10()) { + jj_scanpos = xsp; + if (jj_3R_11()) { + jj_scanpos = xsp; + if (jj_3R_12()) { + jj_scanpos = xsp; + if (jj_3R_13()) { + jj_scanpos = xsp; + if (jj_3R_14()) { + jj_scanpos = xsp; + if (jj_3R_15()) { + jj_scanpos = xsp; + if (jj_3R_16()) { + jj_scanpos = xsp; + if (jj_3R_17()) { + jj_scanpos = xsp; + if (jj_3R_18()) return true; + } + } + } + } + } + } + } + } + return false; + } + + private boolean jj_3R_10() { + if (jj_scan_token(EQUAL_TO)) return true; + return false; + } + + private boolean jj_3R_4() { + if (jj_3R_8()) return true; + return false; + } + + private boolean jj_3R_20() { + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + + private boolean jj_3R_19() { + if (jj_scan_token(STRING)) return true; + return false; + } + + private boolean jj_3_2() { + if (jj_scan_token(WS)) return true; + return false; + } + + private boolean jj_3R_7() { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_19()) { + jj_scanpos = xsp; + if (jj_3R_20()) return true; + } + return false; + } + /** Generated Token Manager. */ public FilterParserTokenManager token_source; SimpleCharStream jj_input_stream; @@ -89,15 +675,20 @@ final public Operator op() throws ParseException { /** Next token. */ public Token jj_nt; private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; private int jj_gen; - final private int[] jj_la1 = new int[2]; + final private int[] jj_la1 = new int[12]; static private int[] jj_la1_0; static { jj_la1_init_0(); } private static void jj_la1_init_0() { - jj_la1_0 = new int[] {0x180,0x60,}; + jj_la1_0 = new int[] {0xc1c00,0x4,0x8,0x4,0x4,0x4,0x4,0xc1c00,0x4,0x4,0xc0000,0x1a3f0,}; } + final private JJCalls[] jj_2_rtns = new JJCalls[6]; + private boolean jj_rescan = false; + private int jj_gc = 0; /** Constructor with InputStream. */ public FilterParser(java.io.InputStream stream) { @@ -110,7 +701,8 @@ public FilterParser(java.io.InputStream stream, String encoding) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } /** Reinitialise. */ @@ -124,7 +716,8 @@ public void ReInit(java.io.InputStream stream, String encoding) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } /** Constructor. */ @@ -134,7 +727,8 @@ public FilterParser(java.io.Reader stream) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } /** Reinitialise. */ @@ -144,7 +738,8 @@ public void ReInit(java.io.Reader stream) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } /** Constructor with generated Token Manager. */ @@ -153,7 +748,8 @@ public FilterParser(FilterParserTokenManager tm) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } /** Reinitialise. */ @@ -162,7 +758,8 @@ public void ReInit(FilterParserTokenManager tm) { token = new Token(); jj_ntk = -1; jj_gen = 0; - for (int i = 0; i < 2; i++) jj_la1[i] = -1; + for (int i = 0; i < 12; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); } private Token jj_consume_token(int kind) throws ParseException { @@ -172,6 +769,16 @@ private Token jj_consume_token(int kind) throws ParseException { jj_ntk = -1; if (token.kind == kind) { jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) c.first = null; + c = c.next; + } + } + } return token; } token = oldToken; @@ -179,6 +786,29 @@ private Token jj_consume_token(int kind) throws ParseException { throw generateParseException(); } + static private final class LookaheadSuccess extends java.lang.Error { } + final private LookaheadSuccess jj_ls = new LookaheadSuccess(); + private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) jj_add_error_token(kind, i); + } + if (jj_scanpos.kind != kind) return true; + if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls; + return false; + } + /** Get the next Token. */ final public Token getNextToken() { @@ -209,16 +839,43 @@ private int jj_ntk() { private java.util.List jj_expentries = new java.util.ArrayList(); private int[] jj_expentry; private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) return; + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + jj_entries_loop: for (java.util.Iterator it = jj_expentries.iterator(); it.hasNext();) { + int[] oldentry = (int[])(it.next()); + if (oldentry.length == jj_expentry.length) { + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + continue jj_entries_loop; + } + } + jj_expentries.add(jj_expentry); + break jj_entries_loop; + } + } + if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } /** Generate ParseException. */ public ParseException generateParseException() { jj_expentries.clear(); - boolean[] la1tokens = new boolean[9]; + boolean[] la1tokens = new boolean[20]; if (jj_kind >= 0) { la1tokens[jj_kind] = true; jj_kind = -1; } - for (int i = 0; i < 2; i++) { + for (int i = 0; i < 12; i++) { if (jj_la1[i] == jj_gen) { for (int j = 0; j < 32; j++) { if ((jj_la1_0[i] & (1< jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + case 1: jj_3_2(); break; + case 2: jj_3_3(); break; + case 3: jj_3_4(); break; + case 4: jj_3_5(); break; + case 5: jj_3_6(); break; + } + } + p = p.next; + } while (p != null); + } catch(LookaheadSuccess ls) { } + } + jj_rescan = false; + } + + private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + } diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.jj b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.jj index b33a608d48a..b80a197b454 100644 --- a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.jj +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParser.jj @@ -1,93 +1,299 @@ -options{ - STATIC=false ; - IGNORE_CASE=true ; +options +{ + STATIC=false; + IGNORE_CASE=false; } PARSER_BEGIN(FilterParser) package org.yamcs.utils.parser; - -/** ID lister. */ -public class FilterParser { +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.yamcs.utils.parser.ast.*; - public static enum Operator { - EQUAL, NOT_EQUAL; - } +@SuppressWarnings({"serial", "unused"}) +public class FilterParser { - public static class Result { - public final String key; - public final Operator op; - public final String value; - - public Result(String key, Operator op, String value) { - this.key = key; - this.op = op; - this.value = value; - } - - @Override - public String toString() { - return "Result [key: '" + key + "', op: '" + op + "', value: '" + value + "']"; - } - } -} + private static final HexFormat HEX = HexFormat.of(); + + private Set fields = new HashSet(); + + private Map> prefixResolvers = new HashMap>(); + private Map> stringResolvers = new HashMap>(); + private Map> numberResolvers = new HashMap>(); + private Map> booleanResolvers = new HashMap>(); + private Map> binaryResolvers = new HashMap>(); + private Map>> enumResolvers = new HashMap>>(); + + private Map>> enumClassByField = new HashMap>>(); + + public void addPrefixField(String field, BiFunction resolver) { + fields.add(field); + prefixResolvers.put(field, resolver); + } + + public void addStringField(String field, Function resolver) { + fields.add(field); + stringResolvers.put(field, resolver); + } + + public > void addEnumField(String field, Class enumClass, Function resolver) { + fields.add(field); + enumResolvers.put(field, resolver); + enumClassByField.put(field, enumClass); + } + + public void addNumberField(String field, Function resolver) { + fields.add(field); + numberResolvers.put(field, resolver); + } + + public void addBooleanField(String field, Function resolver) { + fields.add(field); + booleanResolvers.put(field, resolver); + } + + public void addBinaryField(String field, Function resolver) { + fields.add(field); + binaryResolvers.put(field, resolver); + } + + public BiFunction getPrefixResolver(String field) { + for (var entry : prefixResolvers.entrySet()) { + if (field.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + + public Function getStringResolver(String field) { + return stringResolvers.get(field); + } + public Function getNumberResolver(String field) { + return numberResolvers.get(field); + } + + public Function getBooleanResolver(String field) { + return booleanResolvers.get(field); + } + + public Function getBinaryResolver(String field) { + return binaryResolvers.get(field); + } + + public Function> getEnumResolver(String field) { + return enumResolvers.get(field); + } + + public Class> getEnumClass(String field) { + return enumClassByField.get(field); + } + + /** + * Finds the constant for an Enum label, but case-insensitive. + */ + public > E findEnum(Class enumeration, String value) { + for (E enumConstant : enumeration.getEnumConstants()) { + if (enumConstant.name().compareToIgnoreCase(value) == 0) { + return enumConstant; + } + } + return null; + } +} PARSER_END(FilterParser) -SKIP : + +SPECIAL_TOKEN : { - " " -| "\t" -| "\n" -| "\r" + < SINGLE_LINE_COMMENT: "--"(~["\n","\r"])* ("\n"|"\r"|"\r\n")? > } TOKEN : { - } TOKEN : { - < STRING : (["A"-"Z","a"-"z","0"-"9", ":", "_"])+ > - | +| < EQUAL_TO: "=" > +| < GREATER_THAN: ">" > +| < GREATER_THAN_OR_EQUAL_TO: ">=" > +| < HAS: ":" > +| < LESS_THAN: "<" > +| < LESS_THAN_OR_EQUAL_TO: "<=" > +| < LPAREN: "(" > +| < MINUS: "-" > +| < NOT: "NOT" > +| < NOT_EQUAL_TO: "!=" > +| < OR: "OR" > +| < RE_EQUAL_TO: "=~" > +| < RE_NOT_EQUAL_TO: "!~" > +| < RPAREN: ")" > +} + +TOKEN : +{ + < STRING: ["A"-"Z", "a"-"z", "0"-"9", "_"](["A"-"Z", "a"-"z", "0"-"9", "_", "-", "."])* > +| < QUOTED_STRING: "\"" ( "\\" ~[] //any escaped character | ~["\"","\\"] //any character except quote or backslash - )* - "\"" > + )* + "\"" > +} + +AndExpression parse() : +{ + AndExpression result = null; +} +{ + [ LOOKAHEAD(2) ] [ result = expr() ] [] { return result; } } -Result parse(): -{Result r;} +AndExpression expr() : +{ + AndExpression and; +} { - r= expr() - {return r;} + and = and() [ LOOKAHEAD(2) ] { return and; } } -/** Top level production. */ -Result expr() : +AndExpression and() : { -String key, value; -Operator op; + OrExpression clause; + List clauses = new ArrayList(); } { - key = term() op = op() value = term() { return new Result(key, op, value);} + clause = or() { clauses.add(clause); } + ( LOOKAHEAD(2) [ ] clause = or() { clauses.add(clause); } )* + { + return new AndExpression(clauses); + } } -String term(): +OrExpression or() : +{ + UnaryExpression clause; + List clauses = new ArrayList(); +} +{ + clause = unary() { clauses.add(clause); } + ( LOOKAHEAD(2) clause=unary() { clauses.add(clause); } )* + { + return new OrExpression(clauses); + } +} + +UnaryExpression unary() : +{ + Comparison comparison; + AndExpression expr; +} +{ + LOOKAHEAD(3) + [] [] expr = expr() [] { return new UnaryExpression(expr, true); } +| [] comparison = comparison() { return new UnaryExpression(comparison, true); } +| comparison = comparison() { return new UnaryExpression(comparison, true); } +| expr = expr() { return new UnaryExpression(expr, false); } +| comparison = comparison() { return new UnaryExpression(comparison, false); } +} + +Comparison comparison() : +{ + String comparable; + Token comparableToken; + Token comparatorToken = null; + Comparator comparator = null; + String value = null; + Pattern pattern = null; + byte[] binary = null; +} +{ + comparable = term() { + comparableToken = token; + } + [ LOOKAHEAD(2) [] comparator = comparator() { + comparatorToken = token; + } + [] value = term() ] { + if (comparator != null) { + if (!fields.contains(comparable)) { + boolean prefixMatch = false; + for (String prefix : prefixResolvers.keySet()) { + if (comparable.startsWith(prefix)) { + prefixMatch = true; + break; + } + } + if (!prefixMatch) { + throw new UnknownFieldException(comparable, comparableToken, tokenImage); + } + } + + Class> enumClass = enumClassByField.get(comparable); + if (enumClass != null) { + if (!value.equalsIgnoreCase("null") && findEnum(enumClass, value) == null) { + throw new IncorrectTypeException(value, token, tokenImage); + } + } + + if (binaryResolvers.containsKey(comparable)) { + if (!value.equalsIgnoreCase("null")) { + try { + binary = HEX.parseHex(value); + } catch (NumberFormatException e) { + throw new IncorrectTypeException(value, token, tokenImage); + } + } + } + + if (comparator == Comparator.RE_EQUAL_TO || comparator == Comparator.RE_NOT_EQUAL_TO) { + try { + pattern = Pattern.compile(value); + } catch (PatternSyntaxException e) { + throw new InvalidPatternException(value, token, tokenImage); + } + } + } + + return new Comparison(comparable, comparator, value, pattern, binary); + } +} + +String term() : {} { - {return token.image;} - | {String s = token.image; return s.substring(1, s.length() - 1).replace("\\\"","\"").replace("\\\\","\\");} + { return token.image; } +| { + String s = token.image; + return s.substring(1, s.length() - 1).replace("\\\"","\"").replace("\\\\","\\"); + } } -Operator op(): +Comparator comparator() : {} { - {return Operator.EQUAL;} - | {return Operator.NOT_EQUAL;} - - + { return Comparator.EQUAL_TO; } +| { return Comparator.NOT_EQUAL_TO; } +| { return Comparator.LESS_THAN; } +| { return Comparator.GREATER_THAN; } +| { return Comparator.LESS_THAN_OR_EQUAL_TO; } +| { return Comparator.GREATER_THAN_OR_EQUAL_TO; } +| { return Comparator.HAS; } +| { return Comparator.RE_EQUAL_TO; } +| { return Comparator.RE_NOT_EQUAL_TO; } } diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserConstants.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserConstants.java index 054b92805b3..38070b6994d 100644 --- a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserConstants.java +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserConstants.java @@ -11,13 +11,43 @@ public interface FilterParserConstants { /** End of File. */ int EOF = 0; /** RegularExpression Id. */ - int EQUAL = 5; + int SINGLE_LINE_COMMENT = 1; /** RegularExpression Id. */ - int NOT_EQUAL = 6; + int WS = 2; /** RegularExpression Id. */ - int STRING = 7; + int AND = 3; /** RegularExpression Id. */ - int QUOTED_STRING = 8; + int EQUAL_TO = 4; + /** RegularExpression Id. */ + int GREATER_THAN = 5; + /** RegularExpression Id. */ + int GREATER_THAN_OR_EQUAL_TO = 6; + /** RegularExpression Id. */ + int HAS = 7; + /** RegularExpression Id. */ + int LESS_THAN = 8; + /** RegularExpression Id. */ + int LESS_THAN_OR_EQUAL_TO = 9; + /** RegularExpression Id. */ + int LPAREN = 10; + /** RegularExpression Id. */ + int MINUS = 11; + /** RegularExpression Id. */ + int NOT = 12; + /** RegularExpression Id. */ + int NOT_EQUAL_TO = 13; + /** RegularExpression Id. */ + int OR = 14; + /** RegularExpression Id. */ + int RE_EQUAL_TO = 15; + /** RegularExpression Id. */ + int RE_NOT_EQUAL_TO = 16; + /** RegularExpression Id. */ + int RPAREN = 17; + /** RegularExpression Id. */ + int STRING = 18; + /** RegularExpression Id. */ + int QUOTED_STRING = 19; /** Lexical state. */ int DEFAULT = 0; @@ -25,12 +55,23 @@ public interface FilterParserConstants { /** Literal token values. */ String[] tokenImage = { "", - "\" \"", - "\"\\t\"", - "\"\\n\"", - "\"\\r\"", + "", + "", + "\"AND\"", "\"=\"", + "\">\"", + "\">=\"", + "\":\"", + "\"<\"", + "\"<=\"", + "\"(\"", + "\"-\"", + "\"NOT\"", "\"!=\"", + "\"OR\"", + "\"=~\"", + "\"!~\"", + "\")\"", "", "", }; diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserTokenManager.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserTokenManager.java index d76ccd847c6..fb5c073b924 100644 --- a/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserTokenManager.java +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/FilterParserTokenManager.java @@ -1,5 +1,18 @@ /* Generated By:JavaCC: Do not edit this line. FilterParserTokenManager.java */ package org.yamcs.utils.parser; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.yamcs.utils.parser.ast.*; /** Token Manager. */ public class FilterParserTokenManager implements FilterParserConstants @@ -13,6 +26,25 @@ private final int jjStopStringLiteralDfa_0(int pos, long active0) { switch (pos) { + case 0: + if ((active0 & 0x800L) != 0L) + return 0; + if ((active0 & 0x5008L) != 0L) + { + jjmatchedKind = 18; + return 8; + } + return -1; + case 1: + if ((active0 & 0x1008L) != 0L) + { + jjmatchedKind = 18; + jjmatchedPos = 1; + return 8; + } + if ((active0 & 0x4000L) != 0L) + return 8; + return -1; default : return -1; } @@ -32,11 +64,32 @@ private int jjMoveStringLiteralDfa0_0() switch(curChar) { case 33: - return jjMoveStringLiteralDfa1_0(0x40L); + return jjMoveStringLiteralDfa1_0(0x12000L); + case 40: + return jjStopAtPos(0, 10); + case 41: + return jjStopAtPos(0, 17); + case 45: + return jjStartNfaWithStates_0(0, 11, 0); + case 58: + return jjStopAtPos(0, 7); + case 60: + jjmatchedKind = 8; + return jjMoveStringLiteralDfa1_0(0x200L); case 61: - return jjStopAtPos(0, 5); + jjmatchedKind = 4; + return jjMoveStringLiteralDfa1_0(0x8000L); + case 62: + jjmatchedKind = 5; + return jjMoveStringLiteralDfa1_0(0x40L); + case 65: + return jjMoveStringLiteralDfa1_0(0x8L); + case 78: + return jjMoveStringLiteralDfa1_0(0x1000L); + case 79: + return jjMoveStringLiteralDfa1_0(0x4000L); default : - return jjMoveNfa_0(1, 0); + return jjMoveNfa_0(5, 0); } } private int jjMoveStringLiteralDfa1_0(long active0) @@ -51,19 +104,69 @@ private int jjMoveStringLiteralDfa1_0(long active0) case 61: if ((active0 & 0x40L) != 0L) return jjStopAtPos(1, 6); + else if ((active0 & 0x200L) != 0L) + return jjStopAtPos(1, 9); + else if ((active0 & 0x2000L) != 0L) + return jjStopAtPos(1, 13); + break; + case 78: + return jjMoveStringLiteralDfa2_0(active0, 0x8L); + case 79: + return jjMoveStringLiteralDfa2_0(active0, 0x1000L); + case 82: + if ((active0 & 0x4000L) != 0L) + return jjStartNfaWithStates_0(1, 14, 8); + break; + case 126: + if ((active0 & 0x8000L) != 0L) + return jjStopAtPos(1, 15); + else if ((active0 & 0x10000L) != 0L) + return jjStopAtPos(1, 16); break; default : break; } return jjStartNfa_0(0, active0); } +private int jjMoveStringLiteralDfa2_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_0(0, old0); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(1, active0); + return 2; + } + switch(curChar) + { + case 68: + if ((active0 & 0x8L) != 0L) + return jjStartNfaWithStates_0(2, 3, 8); + break; + case 84: + if ((active0 & 0x1000L) != 0L) + return jjStartNfaWithStates_0(2, 12, 8); + break; + default : + break; + } + return jjStartNfa_0(1, active0); +} +private int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} static final long[] jjbitVec0 = { 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL }; private int jjMoveNfa_0(int startState, int curPos) { int startsAt = 0; - jjnewStateCnt = 6; + jjnewStateCnt = 14; int i = 1; jjstateSet[0] = startState; int kind = 0x7fffffff; @@ -78,33 +181,85 @@ private int jjMoveNfa_0(int startState, int curPos) { switch(jjstateSet[--i]) { - case 1: - if ((0x7ff000000000000L & l) != 0L) + case 5: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 18) + kind = 18; + jjCheckNAdd(8); + } + else if ((0x100002600L & l) != 0L) { - if (kind > 7) - kind = 7; - jjCheckNAdd(0); + if (kind > 2) + kind = 2; + jjCheckNAdd(6); } else if (curChar == 34) jjCheckNAddStates(0, 2); + else if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 0; break; case 0: - if ((0x7ff000000000000L & l) == 0L) + if (curChar != 45) + break; + if (kind > 1) + kind = 1; + jjCheckNAddStates(3, 5); + break; + case 1: + if ((0xffffffffffffdbffL & l) == 0L) break; - if (kind > 7) - kind = 7; - jjCheckNAdd(0); + if (kind > 1) + kind = 1; + jjCheckNAddStates(3, 5); + break; + case 2: + if ((0x2400L & l) != 0L && kind > 1) + kind = 1; break; case 3: - jjCheckNAddStates(0, 2); + if (curChar == 10 && kind > 1) + kind = 1; break; case 4: + if (curChar == 13) + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if ((0x100002600L & l) == 0L) + break; + if (kind > 2) + kind = 2; + jjCheckNAdd(6); + break; + case 7: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(8); + break; + case 8: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(8); + break; + case 9: + if (curChar == 34) + jjCheckNAddStates(0, 2); + break; + case 11: + jjCheckNAddStates(0, 2); + break; + case 12: if ((0xfffffffbffffffffL & l) != 0L) jjCheckNAddStates(0, 2); break; - case 5: - if (curChar == 34 && kind > 8) - kind = 8; + case 13: + if (curChar == 34 && kind > 19) + kind = 19; break; default : break; } @@ -117,22 +272,27 @@ else if (curChar < 128) { switch(jjstateSet[--i]) { - case 1: - case 0: + case 5: + case 8: if ((0x7fffffe87fffffeL & l) == 0L) break; - if (kind > 7) - kind = 7; - jjCheckNAdd(0); + if (kind > 18) + kind = 18; + jjCheckNAdd(8); break; - case 2: + case 1: + if (kind > 1) + kind = 1; + jjAddStates(3, 5); + break; + case 10: if (curChar == 92) - jjstateSet[jjnewStateCnt++] = 3; + jjstateSet[jjnewStateCnt++] = 11; break; - case 3: + case 11: jjCheckNAddStates(0, 2); break; - case 4: + case 12: if ((0xffffffffefffffffL & l) != 0L) jjCheckNAddStates(0, 2); break; @@ -148,8 +308,15 @@ else if (curChar < 128) { switch(jjstateSet[--i]) { - case 3: - case 4: + case 1: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 1) + kind = 1; + jjAddStates(3, 5); + break; + case 11: + case 12: if ((jjbitVec0[i2] & l2) != 0L) jjCheckNAddStates(0, 2); break; @@ -164,33 +331,38 @@ else if (curChar < 128) kind = 0x7fffffff; } ++curPos; - if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + if ((i = jjnewStateCnt) == (startsAt = 14 - (jjnewStateCnt = startsAt))) return curPos; try { curChar = input_stream.readChar(); } catch(java.io.IOException e) { return curPos; } } } static final int[] jjnextStates = { - 2, 4, 5, + 10, 12, 13, 1, 2, 4, }; /** Token literal values. */ public static final String[] jjstrLiteralImages = { -"", null, null, null, null, "\75", "\41\75", null, null, }; +"", null, null, "\101\116\104", "\75", "\76", "\76\75", "\72", "\74", +"\74\75", "\50", "\55", "\116\117\124", "\41\75", "\117\122", "\75\176", "\41\176", +"\51", null, null, }; /** Lexer state names. */ public static final String[] lexStateNames = { "DEFAULT", }; static final long[] jjtoToken = { - 0x1e1L, + 0xffffdL, }; static final long[] jjtoSkip = { - 0x1eL, + 0x2L, +}; +static final long[] jjtoSpecial = { + 0x2L, }; protected SimpleCharStream input_stream; -private final int[] jjrounds = new int[6]; -private final int[] jjstateSet = new int[12]; +private final int[] jjrounds = new int[14]; +private final int[] jjstateSet = new int[28]; protected char curChar; /** Constructor. */ public FilterParserTokenManager(SimpleCharStream stream){ @@ -217,7 +389,7 @@ private void ReInitRounds() { int i; jjround = 0x80000001; - for (i = 6; i-- > 0;) + for (i = 14; i-- > 0;) jjrounds[i] = 0x80000000; } @@ -271,6 +443,7 @@ protected Token jjFillToken() /** Get the next Token. */ public Token getNextToken() { + Token specialToken = null; Token matchedToken; int curPos = 0; @@ -285,14 +458,10 @@ public Token getNextToken() { jjmatchedKind = 0; matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; return matchedToken; } - try { input_stream.backup(0); - while (curChar <= 32 && (0x100002600L & (1L << curChar)) != 0L) - curChar = input_stream.BeginToken(); - } - catch (java.io.IOException e1) { continue EOFLoop; } jjmatchedKind = 0x7fffffff; jjmatchedPos = 0; curPos = jjMoveStringLiteralDfa0_0(); @@ -303,10 +472,22 @@ public Token getNextToken() if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) { matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; return matchedToken; } else { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } continue EOFLoop; } } diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/IncorrectTypeException.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/IncorrectTypeException.java new file mode 100644 index 00000000000..626f5210ee8 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/IncorrectTypeException.java @@ -0,0 +1,22 @@ +package org.yamcs.utils.parser; + +@SuppressWarnings("serial") +public class IncorrectTypeException extends ParseException { + + private String value; + + public IncorrectTypeException(String value, Token currentToken, String[] tokenImage) { + super("Value '" + value + "' is of incorrect type"); + this.value = value; + this.currentToken = currentToken; + this.tokenImage = tokenImage; + } + + public String getValue() { + return value; + } + + public String getKind() { + return tokenImage[currentToken.kind]; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/InvalidPatternException.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/InvalidPatternException.java new file mode 100644 index 00000000000..7d766e68e6e --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/InvalidPatternException.java @@ -0,0 +1,22 @@ +package org.yamcs.utils.parser; + +@SuppressWarnings("serial") +public class InvalidPatternException extends ParseException { + + private String pattern; + + public InvalidPatternException(String pattern, Token currentToken, String[] tokenImage) { + super("Invalid regex '" + pattern + "'"); + this.pattern = pattern; + this.currentToken = currentToken; + this.tokenImage = tokenImage; + } + + public String getPattern() { + return pattern; + } + + public String getKind() { + return tokenImage[currentToken.kind]; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/TokenMgrError.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/TokenMgrError.java index 4032b5a2227..9631446158d 100644 --- a/yamcs-core/src/main/java/org/yamcs/utils/parser/TokenMgrError.java +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/TokenMgrError.java @@ -1,147 +1,153 @@ -/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 5.0 */ /* JavaCCOptions: */ package org.yamcs.utils.parser; /** Token Manager Error. */ -public class TokenMgrError extends Error -{ - - /** - * The version identifier for this Serializable class. - * Increment only if the serialized form of the - * class changes. - */ - private static final long serialVersionUID = 1L; - - /* - * Ordinals for various reasons why an Error of this type can be thrown. - */ - - /** - * Lexical error occurred. - */ - static final int LEXICAL_ERROR = 0; - - /** - * An attempt was made to create a second instance of a static token manager. - */ - static final int STATIC_LEXER_ERROR = 1; - - /** - * Tried to change to an invalid lexical state. - */ - static final int INVALID_LEXICAL_STATE = 2; - - /** - * Detected (and bailed out of) an infinite loop in the token manager. - */ - static final int LOOP_DETECTED = 3; - - /** - * Indicates the reason why the exception is thrown. It will have - * one of the above 4 values. - */ - int errorCode; - - /** - * Replaces unprintable characters by their escaped (or unicode escaped) - * equivalents in the given string - */ - protected static final String addEscapes(String str) { - StringBuffer retval = new StringBuffer(); - char ch; - for (int i = 0; i < str.length(); i++) { - switch (str.charAt(i)) - { - case 0 : - continue; - case '\b': - retval.append("\\b"); - continue; - case '\t': - retval.append("\\t"); - continue; - case '\n': - retval.append("\\n"); - continue; - case '\f': - retval.append("\\f"); - continue; - case '\r': - retval.append("\\r"); - continue; - case '\"': - retval.append("\\\""); - continue; - case '\'': - retval.append("\\\'"); - continue; - case '\\': - retval.append("\\\\"); - continue; - default: - if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { - String s = "0000" + Integer.toString(ch, 16); - retval.append("\\u" + s.substring(s.length() - 4, s.length())); - } else { - retval.append(ch); - } - continue; - } +public class TokenMgrError extends Error { + + /** + * The version identifier for this Serializable class. Increment only if the serialized form of the class + * changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) { + case 0: + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); } - return retval.toString(); - } - - /** - * Returns a detailed message for the Error when it is thrown by the - * token manager to indicate a lexical error. - * Parameters : - * EOFSeen : indicates if EOF caused the lexical error - * curLexState : lexical state in which this error occurred - * errorLine : line number when the error occurred - * errorColumn : column number when the error occurred - * errorAfter : prefix that was seen before this error occurred - * curchar : the offending character - * Note: You can customize the lexical error message by modifying this method. - */ - protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { - return("Lexical error at line " + - errorLine + ", column " + - errorColumn + ". Encountered: " + - (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + - "after : \"" + addEscapes(errorAfter) + "\""); - } - - /** - * You can also modify the body of this method to customize your error messages. - * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not - * of end-users concern, so you can return something like : - * - * "Internal Error : Please file a bug report .... " - * - * from this method for such cases in the release version of your parser. - */ - public String getMessage() { - return super.getMessage(); - } - - /* - * Constructors of various flavors follow. - */ - - /** No arg constructor. */ - public TokenMgrError() { - } - - /** Constructor with message and reason. */ - public TokenMgrError(String message, int reason) { - super(message); - errorCode = reason; - } - - /** Full Constructor. */ - public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { - this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); - } + + /** + * Returns a detailed message for the Error when it is thrown by the token manager to indicate a lexical error. + * Parameters : EOFSeen : indicates if EOF caused the lexical error curLexState : lexical state in which this error + * occurred errorLine : line number when the error occurred errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred curchar : the offending character Note: You can + * customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, + String errorAfter, char curChar) { + return ("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " + : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int) curChar + "), ") + + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. For example, cases like + * LOOP_DETECTED and INVALID_LEXICAL_STATE are not of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + @Override + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** No arg constructor. */ + public TokenMgrError() { + } + + /** Constructor with message and reason. */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** Full Constructor. */ + /* + * Customized for Yamcs to expose error context on lexical errors. + * + * Note that javacc will preserve changes to this class (while throwing + * a non-meaningful warning message) + */ + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, + int reason) { + this("Invalid syntax", reason); + this.errorLine = errorLine; + this.errorColumn = errorColumn; + } + + // Custom fields + public int errorLine = -1; + public int errorColumn = -1; + } /* JavaCC - OriginalChecksum=2d23260405996cde0da4ac168a4e7948 (do not edit this line) */ diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/UnknownFieldException.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/UnknownFieldException.java new file mode 100644 index 00000000000..198a2eb9c2b --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/UnknownFieldException.java @@ -0,0 +1,22 @@ +package org.yamcs.utils.parser; + +@SuppressWarnings("serial") +public class UnknownFieldException extends ParseException { + + private String field; + + public UnknownFieldException(String field, Token currentToken, String[] tokenImage) { + super("Field not found '" + field + "'"); + this.field = field; + this.currentToken = currentToken; + this.tokenImage = tokenImage; + } + + public String getField() { + return field; + } + + public String getKind() { + return tokenImage[currentToken.kind]; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/AndExpression.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/AndExpression.java new file mode 100644 index 00000000000..d1eaec663ff --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/AndExpression.java @@ -0,0 +1,27 @@ +package org.yamcs.utils.parser.ast; + +import java.util.List; + +public class AndExpression implements Node { + + private List clauses; + + public AndExpression(List clauses) { + this.clauses = clauses; + } + + public List getClauses() { + return clauses; + } + + @Override + public String toString(String indent) { + StringBuilder buf = new StringBuilder(indent) + .append(getClass().getSimpleName()) + .append("\n"); + for (OrExpression clause : clauses) { + buf.append(clause.toString(indent + " |")); + } + return buf.toString(); + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparator.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparator.java new file mode 100644 index 00000000000..292d8b6af01 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparator.java @@ -0,0 +1,13 @@ +package org.yamcs.utils.parser.ast; + +public enum Comparator { + EQUAL_TO, + GREATER_THAN, + GREATER_THAN_OR_EQUAL_TO, + HAS, + LESS_THAN, + LESS_THAN_OR_EQUAL_TO, + NOT_EQUAL_TO, + RE_EQUAL_TO, + RE_NOT_EQUAL_TO; +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparison.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparison.java new file mode 100644 index 00000000000..3eb29b9b259 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Comparison.java @@ -0,0 +1,29 @@ +package org.yamcs.utils.parser.ast; + +import java.util.regex.Pattern; + +public class Comparison implements Node { + + public final String comparable; + public final Comparator comparator; + public final String value; + + public final Pattern pattern; + public final byte[] binary; + + public Comparison(String comparable, Comparator comparator, String value, Pattern pattern, byte[] binary) { + this.comparable = comparable; + this.comparator = comparator; + this.value = value; + this.pattern = pattern; + this.binary = binary; + } + + @Override + public String toString(String indent) { + return indent + getClass().getSimpleName() + "\n" + + indent + " |" + comparable + "\n" + + indent + " |" + comparator + "\n" + + indent + " |" + value; + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Node.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Node.java new file mode 100644 index 00000000000..475610b1f99 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/Node.java @@ -0,0 +1,6 @@ +package org.yamcs.utils.parser.ast; + +public interface Node { + + String toString(String indent); +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/OrExpression.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/OrExpression.java new file mode 100644 index 00000000000..796fa3865a4 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/OrExpression.java @@ -0,0 +1,27 @@ +package org.yamcs.utils.parser.ast; + +import java.util.List; + +public class OrExpression implements Node { + + private List clauses; + + public OrExpression(List clauses) { + this.clauses = clauses; + } + + public List getClauses() { + return clauses; + } + + @Override + public String toString(String indent) { + StringBuilder buf = new StringBuilder(indent) + .append(getClass().getSimpleName()) + .append("\n"); + for (UnaryExpression clause : clauses) { + buf.append(clause.toString(indent + " |")); + } + return buf.toString(); + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/UnaryExpression.java b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/UnaryExpression.java new file mode 100644 index 00000000000..7ac7a9e4b45 --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/ast/UnaryExpression.java @@ -0,0 +1,46 @@ +package org.yamcs.utils.parser.ast; + +public class UnaryExpression implements Node { + + private Comparison comparison; + private AndExpression andExpression; + private boolean not; + + public UnaryExpression(Comparison comparison, boolean not) { + this.comparison = comparison; + this.not = not; + } + + public UnaryExpression(AndExpression andExpression, boolean not) { + this.andExpression = andExpression; + this.not = not; + } + + public Comparison getComparison() { + return comparison; + } + + public boolean isNot() { + return not; + } + + public AndExpression getAndExpression() { + return andExpression; + } + + @Override + public String toString(String indent) { + StringBuilder buf = new StringBuilder(indent) + .append(getClass().getSimpleName()) + .append("\n"); + if (not) { + buf.append(indent).append(" |").append("NOT\n"); + } + if (comparison != null) { + buf.append(comparison.toString(indent + " |")).append("\n"); + } else { + buf.append(andExpression.toString(indent + " |")); + } + return buf.toString(); + } +} diff --git a/yamcs-core/src/main/java/org/yamcs/utils/parser/javacc-invocation.sh b/yamcs-core/src/main/java/org/yamcs/utils/parser/javacc-invocation.sh new file mode 100755 index 00000000000..8f0fd116f5e --- /dev/null +++ b/yamcs-core/src/main/java/org/yamcs/utils/parser/javacc-invocation.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +javacc -nostatic -JDK_VERSION=1.6 FilterParser.jj diff --git a/yamcs-core/src/test/java/org/yamcs/utils/FilterParserTest.java b/yamcs-core/src/test/java/org/yamcs/utils/FilterParserTest.java index 5e6a6c114fc..004f6f9a212 100644 --- a/yamcs-core/src/test/java/org/yamcs/utils/FilterParserTest.java +++ b/yamcs-core/src/test/java/org/yamcs/utils/FilterParserTest.java @@ -1,32 +1,335 @@ package org.yamcs.utils; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.StringReader; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; import org.junit.jupiter.api.Test; -import org.yamcs.utils.parser.FilterParser; +import org.yamcs.protobuf.Event.EventSeverity; +import org.yamcs.utils.parser.Filter; +import org.yamcs.utils.parser.IncorrectTypeException; +import org.yamcs.utils.parser.ParseException; +import org.yamcs.utils.parser.UnknownFieldException; public class FilterParserTest { + private static final HexFormat HEX = HexFormat.of(); + + private Item a = new Item("round horn", EventSeverity.INFO, false, 1, HEX.parseHex("aabb")); + private Item b = new Item("wacky hippo", EventSeverity.WATCH, true, 2, HEX.parseHex("bbcc")); + private Item c = new Item("icy wombat", EventSeverity.WARNING, true, 3, HEX.parseHex("ccdd")); + private Item d = new Item("lush ghost", EventSeverity.DISTRESS, false, 4, HEX.parseHex("ddee")); + private Item e = new Item("rich sea", EventSeverity.CRITICAL, false, 5, HEX.parseHex("eeff")); + private Item f = new Item("heavy lake", null, false, 6, HEX.parseHex("ff00")); + + private List allItems = asList(a, b, c, d, e, f); + + private List filterItems(ItemFilter filter) { + return allItems.stream().filter(filter::matches).toList(); + } + + @Test + public void testEmptyFilter() throws ParseException { + var filter = new ItemFilter(""); + assertEquals(allItems, filterItems(filter)); + + filter = new ItemFilter("\n"); + assertEquals(allItems, filterItems(filter)); + } + + @Test + public void testRegex() throws ParseException { + var filter = new ItemFilter("name =~ \"s.a\""); + assertEquals(asList(e), filterItems(filter)); + + filter = new ItemFilter("name =~ \"^r.*$\""); + assertEquals(asList(a, e), filterItems(filter)); + + filter = new ItemFilter("name !~ \"^r.*$\""); + assertEquals(asList(b, c, d, f), filterItems(filter)); + + filter = new ItemFilter("name =~ \"SeA\""); + assertEquals(asList(), filterItems(filter), "Regex is case-sensitive"); + + filter = new ItemFilter("name =~ \"(?i)SeA\""); // Case-insensitive + assertEquals(asList(e), filterItems(filter)); + } + + @Test + public void testNullField() throws ParseException { + var filter = new ItemFilter("severity=null"); + assertEquals(asList(f), filterItems(filter)); + + var nullString = new Item(null, EventSeverity.INFO, false, 1, null); + var extendedItems = new ArrayList<>(allItems); + extendedItems.add(nullString); + filter = new ItemFilter("name = null"); + assertEquals(asList(nullString), extendedItems.stream().filter(filter::matches).toList()); + + var nullBoolean = new Item("swift gopher", EventSeverity.INFO, null, 1, null); + extendedItems = new ArrayList<>(allItems); + extendedItems.add(nullBoolean); + filter = new ItemFilter("animal = null"); + assertEquals(asList(nullBoolean), extendedItems.stream().filter(filter::matches).toList()); + + var nullNumber = new Item("swift gopher", EventSeverity.INFO, true, null, null); + extendedItems = new ArrayList<>(allItems); + extendedItems.add(nullNumber); + filter = new ItemFilter("order = null"); + assertEquals(asList(nullNumber), extendedItems.stream().filter(filter::matches).toList()); + + var nullBinary = new Item("swift gopher", EventSeverity.INFO, true, 1, null); + extendedItems = new ArrayList<>(allItems); + extendedItems.add(nullBinary); + filter = new ItemFilter("binary = null"); + assertEquals(asList(nullBinary), extendedItems.stream().filter(filter::matches).toList()); + } + + @Test + public void testPrecendence() throws ParseException { + var filter = new ItemFilter("lush AND (ghost OR wombat)"); + assertEquals(asList(d), filterItems(filter)); + + filter = new ItemFilter("(lush AND ghost) OR wombat"); + assertEquals(asList(c, d), filterItems(filter)); + + // Unlike programming languages, OR has higher precedence than AND + filter = new ItemFilter("lush AND ghost OR wombat"); + assertEquals(asList(d), filterItems(filter)); + } + + @Test + public void testLogicalOperators() throws ParseException { + var filter = new ItemFilter("name=\"icy wombat\" OR severity=DISTRESS"); + assertEquals(asList(c, d), filterItems(filter)); + + filter = new ItemFilter("name=\"icy wombat\" AND severity=DISTRESS"); + assertEquals(asList(), filterItems(filter)); + + filter = new ItemFilter("name=\"icy wombat\" AND severity=WARNING"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("name=\"icy wombat\" AND (severity=WARNING OR severity=DISTRESS)"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("name=\"icy wombat\" OR (severity=WARNING OR severity=DISTRESS)"); + assertEquals(asList(c, d), filterItems(filter)); + + filter = new ItemFilter("(name=\"icy wombat\" OR severity=CRITICAL) OR severity=DISTRESS"); + assertEquals(asList(c, d, e), filterItems(filter)); + } + + @Test + public void testQuotedString() throws ParseException { + var filter = new ItemFilter("name=\"icy wombat\""); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("name != \"icy wombat\""); + assertEquals(asList(a, b, d, e, f), filterItems(filter)); + + filter = new ItemFilter("NOT ( name = \"icy wombat\" )"); + assertEquals(asList(a, b, d, e, f), filterItems(filter)); + + filter = new ItemFilter("name > \"mmm\""); + assertEquals(asList(a, b, e), filterItems(filter)); + } + + @Test + public void testEnum() throws ParseException { + var filter = new ItemFilter("severity = INFO"); + assertEquals(asList(a), filterItems(filter)); + + filter = new ItemFilter("severity = info"); + assertEquals(asList(a), filterItems(filter), "Must be case-insensitive"); + + filter = new ItemFilter("severity >= DISTRESS"); + assertEquals(asList(d, e), filterItems(filter)); + + var exc = assertThrows(IncorrectTypeException.class, () -> { + new ItemFilter("severity >= INFOooo"); + }); + assertEquals(exc.getValue(), "INFOooo"); + + var exc2 = assertThrows(UnknownFieldException.class, () -> { + new ItemFilter("severityy >= INFO"); + }); + assertEquals(exc2.getField(), "severityy"); + } + + @Test + public void testBoolean() throws ParseException { + var filter = new ItemFilter("animal=true"); + assertEquals(asList(b, c), filterItems(filter)); + + filter = new ItemFilter("animal=True"); + assertEquals(asList(b, c), filterItems(filter)); + + filter = new ItemFilter("animal=false"); + assertEquals(asList(a, d, e, f), filterItems(filter)); + + filter = new ItemFilter("animal=False"); + assertEquals(asList(a, d, e, f), filterItems(filter)); + } + + @Test + public void testNumber() throws ParseException { + var filter = new ItemFilter("order < \"3.2\""); + assertEquals(asList(a, b, c), filterItems(filter)); + } + + @Test + public void testBinary() throws ParseException { + var filter = new ItemFilter("binary=ccdd"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("binary=CCDD"); + assertEquals(asList(c), filterItems(filter), "Case-insensitive"); + + filter = new ItemFilter("binary=\"ccdd\""); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("binary:cc"); + assertEquals(asList(b, c), filterItems(filter)); + + filter = new ItemFilter("binary < cc"); + assertEquals(asList(a, b), filterItems(filter)); + } + + @Test + public void testQuotedEnum() throws ParseException { + var filter = new ItemFilter("severity = \"INFO\""); + assertEquals(asList(a), filterItems(filter)); + + filter = new ItemFilter("severity >= \"DISTRESS\""); + assertEquals(asList(d, e), filterItems(filter)); + + var exc = assertThrows(IncorrectTypeException.class, () -> { + new ItemFilter("severity >= \"INFOooo\""); + }); + assertEquals(exc.getValue(), "INFOooo"); + + var exc2 = assertThrows(UnknownFieldException.class, () -> { + new ItemFilter("severityy >= \"INFO\""); + }); + assertEquals(exc2.getField(), "severityy"); + } + @Test - public void test1() throws Exception { - FilterParser fp = new FilterParser(new StringReader("a=b")); - FilterParser.Result r = fp.parse(); - assertEquals("a", r.key); - assertEquals(FilterParser.Operator.EQUAL, r.op); - assertEquals("b", r.value); - - fp.ReInit(new StringReader("\"cucu\" != bau")); - r = fp.parse(); - assertEquals("cucu", r.key); - assertEquals(FilterParser.Operator.NOT_EQUAL, r.op); - assertEquals("bau", r.value); - - fp.ReInit(new StringReader("tag:cucu=bau")); - r = fp.parse(); - assertEquals("tag:cucu", r.key); - assertEquals(FilterParser.Operator.EQUAL, r.op); - assertEquals("bau", r.value); + public void testGlobalRestriction() throws ParseException { + var filter = new ItemFilter("wombat"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("hippo OR wombat"); + assertEquals(asList(b, c), filterItems(filter)); + + filter = new ItemFilter("-hippo"); + assertEquals(asList(a, c, d, e, f), filterItems(filter)); + + filter = new ItemFilter("-wombat"); + assertEquals(asList(a, b, d, e, f), filterItems(filter)); + + filter = new ItemFilter("-wombat AND -hippo"); + assertEquals(asList(a, d, e, f), filterItems(filter)); + + filter = new ItemFilter("NOT wombat AND NOT hippo"); + assertEquals(asList(a, d, e, f), filterItems(filter)); + + filter = new ItemFilter("wom AND bat"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("wom bat"); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter("wom -bat"); + assertEquals(asList(), filterItems(filter)); + + assertThrows(ParseException.class, () -> { + new ItemFilter("- wombat"); + }, "No space allowed after minus sign"); + } + + @Test + public void testCaseSensitiveLogicalOperators() throws ParseException { + var filter = new ItemFilter("wombat OR hippo"); + assertEquals(asList(b, c), filterItems(filter)); + + filter = new ItemFilter("wombat or hippo"); // Same as: wombat AND or AND hippo + assertEquals(asList(), filterItems(filter)); + } + + @Test + public void testMultilineAndComments() throws ParseException { + var filter = new ItemFilter(""" + -wombat + wombat + """); + assertEquals(asList(), filterItems(filter)); + + filter = new ItemFilter(""" + ---wombat + wombat + """); + assertEquals(asList(c), filterItems(filter)); + + filter = new ItemFilter(""" + -wombat + --wombat + """); + assertEquals(asList(a, b, d, e, f), filterItems(filter)); + + // Intentional EOF instead of newline, following final line comment + filter = new ItemFilter(""" + --nothing + --but + --comments"""); + assertEquals(allItems, filterItems(filter)); + } + + @Test + public void testHas() throws ParseException { + var filter = new ItemFilter("name:wom"); + assertEquals(asList(c), filterItems(filter)); + } + + @Test + public void testPrefix() throws ParseException { + var filter = new ItemFilter("label.name=\"icy wombat\""); + assertEquals(asList(c), filterItems(filter)); + } + + public static record Item(String name, EventSeverity severity, Boolean animal, Integer order, byte[] binary) { + @Override + public String toString() { + return name; + } + } + + public static class ItemFilter extends Filter { + + public ItemFilter(String query) throws ParseException { + super(query); + addStringField("name", item -> item.name()); + addEnumField("severity", EventSeverity.class, item -> item.severity()); + addBooleanField("animal", item -> item.animal()); + addNumberField("order", item -> item.order()); + addBinaryField("binary", item -> item.binary()); + addPrefixField("label.", (item, field) -> { + if (field.equals("label.name")) { + return item.name; + } else { + return null; + } + }); + parse(); + } + + @Override + protected boolean matchesLiteral(Item item, String literal) { + return item.name().toLowerCase().contains(literal); + } } } diff --git a/yamcs-web/src/main/java/org/yamcs/web/WebApi.java b/yamcs-web/src/main/java/org/yamcs/web/WebApi.java deleted file mode 100644 index 1c71863a1a6..00000000000 --- a/yamcs-web/src/main/java/org/yamcs/web/WebApi.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.yamcs.web; - -import static org.yamcs.web.WebPlugin.CONFIG_DISPLAY_BUCKET; -import static org.yamcs.web.WebPlugin.CONFIG_STACK_BUCKET; - -import org.yamcs.YamcsServer; -import org.yamcs.api.Observer; -import org.yamcs.http.Context; -import org.yamcs.http.NotFoundException; -import org.yamcs.web.api.AbstractWebApi; -import org.yamcs.web.api.GetInstanceConfigurationRequest; -import org.yamcs.web.api.InstanceConfiguration; - -/** - * Extension routes to Yamcs HTTP API for use by the Web UI only. - */ -public class WebApi extends AbstractWebApi { - - /** - * Get instance-level Web UI configuration options. - */ - @Override - public void getInstanceConfiguration(Context ctx, GetInstanceConfigurationRequest request, - Observer observer) { - var yamcs = YamcsServer.getServer(); - var yamcsInstance = yamcs.getInstance(request.getInstance()); - if (yamcsInstance == null) { - throw new NotFoundException("No such instance"); - } - - var pluginName = yamcs.getPluginManager().getMetadata(WebPlugin.class).getName(); - - var globalConfig = yamcs.getConfig().getConfigOrEmpty(pluginName); - var instanceConfig = yamcsInstance.getConfig().getConfigOrEmpty(pluginName); - - var b = InstanceConfiguration.newBuilder(); - - var displayBucket = globalConfig.getString(CONFIG_DISPLAY_BUCKET); - if (instanceConfig.containsKey(CONFIG_DISPLAY_BUCKET)) { - displayBucket = instanceConfig.getString(CONFIG_DISPLAY_BUCKET); - } - b.setDisplayBucket(displayBucket); - - var stackBucket = globalConfig.getString(CONFIG_STACK_BUCKET); - if (instanceConfig.containsKey(CONFIG_STACK_BUCKET)) { - stackBucket = instanceConfig.getString(CONFIG_STACK_BUCKET); - } - b.setStackBucket(stackBucket); - - observer.complete(b.build()); - } -} diff --git a/yamcs-web/src/main/java/org/yamcs/web/WebPlugin.java b/yamcs-web/src/main/java/org/yamcs/web/WebPlugin.java index 94f472c5f81..36919752fb0 100644 --- a/yamcs-web/src/main/java/org/yamcs/web/WebPlugin.java +++ b/yamcs-web/src/main/java/org/yamcs/web/WebPlugin.java @@ -23,13 +23,14 @@ import org.yamcs.protobuf.YamcsInstance.InstanceState; import org.yamcs.security.SystemPrivilege; import org.yamcs.templating.ParseException; +import org.yamcs.web.api.WebApi; import org.yamcs.yarch.Bucket; import org.yamcs.yarch.YarchDatabase; public class WebPlugin extends AbstractPlugin implements CommandOptionListener { - static final String CONFIG_DISPLAY_BUCKET = "displayBucket"; - static final String CONFIG_STACK_BUCKET = "stackBucket"; + public static final String CONFIG_DISPLAY_BUCKET = "displayBucket"; + public static final String CONFIG_STACK_BUCKET = "stackBucket"; /** * Allows access to the Admin Area. diff --git a/yamcs-web/src/main/java/org/yamcs/web/api/ParseFilterObserver.java b/yamcs-web/src/main/java/org/yamcs/web/api/ParseFilterObserver.java new file mode 100644 index 00000000000..7ccb07e0d0b --- /dev/null +++ b/yamcs-web/src/main/java/org/yamcs/web/api/ParseFilterObserver.java @@ -0,0 +1,49 @@ +package org.yamcs.web.api; + +import org.yamcs.api.FilterSyntaxException; +import org.yamcs.api.Observer; +import org.yamcs.http.BadRequestException; +import org.yamcs.http.api.EventFilterFactory; + +public class ParseFilterObserver implements Observer { + + private Observer responseObserver; + + public ParseFilterObserver(Observer responseObserver) { + this.responseObserver = responseObserver; + } + + @Override + public void next(ParseFilterRequest request) { + try { + switch (request.getResource()) { + case "events": + EventFilterFactory.create(request.getFilter()); + break; + default: + throw new IllegalStateException("Unexpected resource: '" + request.getResource() + "'"); + } + + responseObserver.next(ParseFilterData.getDefaultInstance()); + } catch (BadRequestException e) { + var detail = (FilterSyntaxException) e.getDetail(); + var responseb = ParseFilterData.newBuilder() + .setErrorMessage(e.getMessage()) + .setBeginLine(detail.getBeginLine()) + .setBeginColumn(detail.getBeginColumn()) + .setEndLine(detail.getEndLine()) + .setEndColumn(detail.getEndColumn()); + responseObserver.next(responseb.build()); + } + } + + @Override + public void completeExceptionally(Throwable t) { + // NOP + } + + @Override + public void complete() { + // NOP + } +} diff --git a/yamcs-web/src/main/java/org/yamcs/web/api/WebApi.java b/yamcs-web/src/main/java/org/yamcs/web/api/WebApi.java new file mode 100644 index 00000000000..1467e7e749d --- /dev/null +++ b/yamcs-web/src/main/java/org/yamcs/web/api/WebApi.java @@ -0,0 +1,183 @@ +package org.yamcs.web.api; + +import static org.yamcs.web.WebPlugin.CONFIG_DISPLAY_BUCKET; +import static org.yamcs.web.WebPlugin.CONFIG_STACK_BUCKET; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; + +import org.yamcs.YamcsServer; +import org.yamcs.api.Observer; +import org.yamcs.http.BadRequestException; +import org.yamcs.http.Context; +import org.yamcs.http.NotFoundException; +import org.yamcs.http.api.InstancesApi; +import org.yamcs.http.api.StreamFactory; +import org.yamcs.web.WebPlugin; +import org.yamcs.web.db.Query; +import org.yamcs.web.db.QueryDb; +import org.yamcs.yarch.SqlBuilder; +import org.yamcs.yarch.Stream; +import org.yamcs.yarch.StreamSubscriber; +import org.yamcs.yarch.Tuple; + +import com.google.protobuf.Empty; + +/** + * Extension routes to Yamcs HTTP API for use by the Web UI only. + */ +public class WebApi extends AbstractWebApi { + + /** + * Get instance-level Web UI configuration options. + */ + @Override + public void getInstanceConfiguration(Context ctx, GetInstanceConfigurationRequest request, + Observer observer) { + var yamcs = YamcsServer.getServer(); + var yamcsInstance = yamcs.getInstance(request.getInstance()); + if (yamcsInstance == null) { + throw new NotFoundException("No such instance"); + } + + var pluginName = yamcs.getPluginManager().getMetadata(WebPlugin.class).getName(); + + var globalConfig = yamcs.getConfig().getConfigOrEmpty(pluginName); + var instanceConfig = yamcsInstance.getConfig().getConfigOrEmpty(pluginName); + + var b = InstanceConfiguration.newBuilder(); + + var displayBucket = globalConfig.getString(CONFIG_DISPLAY_BUCKET); + if (instanceConfig.containsKey(CONFIG_DISPLAY_BUCKET)) { + displayBucket = instanceConfig.getString(CONFIG_DISPLAY_BUCKET); + } + b.setDisplayBucket(displayBucket); + + var stackBucket = globalConfig.getString(CONFIG_STACK_BUCKET); + if (instanceConfig.containsKey(CONFIG_STACK_BUCKET)) { + stackBucket = instanceConfig.getString(CONFIG_STACK_BUCKET); + } + b.setStackBucket(stackBucket); + + observer.complete(b.build()); + } + + @Override + public void listQueries(Context ctx, ListQueriesRequest request, Observer observer) { + var instance = InstancesApi.verifyInstance(request.getInstance()); + + // Not used, but must make sure the table is created + QueryDb.getInstance(instance); + + var sqlb = new SqlBuilder(QueryDb.TABLE_NAME); + + var queries = new ArrayList(); + StreamFactory.stream(instance, sqlb.toString(), sqlb.getQueryArguments(), new StreamSubscriber() { + + @Override + public void onTuple(Stream stream, Tuple tuple) { + var query = new Query(tuple); + queries.add(query); + } + + @Override + public void streamClosed(Stream stream) { + Collections.sort(queries); + + var responseb = ListQueriesResponse.newBuilder(); + queries.forEach(query -> { + var info = toQueryInfo(ctx, query); + responseb.addQueries(info); + }); + + observer.complete(responseb.build()); + } + }); + } + + @Override + public void createQuery(Context ctx, CreateQueryRequest request, Observer observer) { + var instance = InstancesApi.verifyInstance(request.getInstance()); + + var name = request.getName(); + var query = new Query( + UUID.randomUUID(), + request.getResource(), + name, + request.getShared() ? ctx.user.getId() : null, + request.getQuery()); + + var db = QueryDb.getInstance(instance); + db.insert(query); + observer.complete(toQueryInfo(ctx, query)); + } + + @Override + public void updateQuery(Context ctx, UpdateQueryRequest request, Observer observer) { + var instance = InstancesApi.verifyInstance(request.getInstance()); + var db = QueryDb.getInstance(instance); + var query = verifyQuery(db, request.getId()); + + if (request.hasName()) { + query.setName(request.getName()); + } + if (request.hasShared()) { + if (request.getShared()) { + query.setUserId(null); + } else { + query.setUserId(ctx.user.getId()); + } + } + if (request.hasQuery()) { + query.setQuery(request.getQuery()); + } + + db.update(query); + observer.complete(toQueryInfo(ctx, query)); + } + + @Override + public void deleteQuery(Context ctx, DeleteQueryRequest request, Observer observer) { + var instance = InstancesApi.verifyInstance(request.getInstance()); + var db = QueryDb.getInstance(instance); + var query = verifyQuery(db, request.getId()); + + db.delete(query.getId()); + observer.complete(Empty.getDefaultInstance()); + } + + @Override + public Observer parseFilter(Context ctx, Observer observer) { + var clientObserver = new ParseFilterObserver(observer); + observer.setCancelHandler(() -> clientObserver.complete()); + return clientObserver; + } + + private static QueryInfo toQueryInfo(Context ctx, Query query) { + var queryb = QueryInfo.newBuilder() + .setId(query.getId().toString()) + .setName(query.getName()) + .setShared(!Objects.equals(ctx.user.getId(), query.getUserId())) + .setQuery(query.getQuery()); + return queryb.build(); + } + + public static Query verifyQuery(QueryDb db, String id) { + var queryId = verifyId(id); + var query = db.getById(queryId); + if (query == null) { + throw new NotFoundException("Query not found"); + } + return query; + } + + private static UUID verifyId(String id) { + try { + return UUID.fromString(id); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Invalid identifier '" + id + "'"); + } + } +} diff --git a/yamcs-web/src/main/java/org/yamcs/web/db/Query.java b/yamcs-web/src/main/java/org/yamcs/web/db/Query.java new file mode 100644 index 00000000000..2dcafa08df2 --- /dev/null +++ b/yamcs-web/src/main/java/org/yamcs/web/db/Query.java @@ -0,0 +1,92 @@ +package org.yamcs.web.db; + +import static org.yamcs.web.db.QueryDb.CNAME_ID; +import static org.yamcs.web.db.QueryDb.CNAME_NAME; +import static org.yamcs.web.db.QueryDb.CNAME_QUERY; +import static org.yamcs.web.db.QueryDb.CNAME_RESOURCE; +import static org.yamcs.web.db.QueryDb.CNAME_USER_ID; +import static org.yamcs.web.db.QueryDb.STRUCT_TYPE; + +import java.util.UUID; + +import org.yamcs.yarch.DataType; +import org.yamcs.yarch.Tuple; + +import com.google.protobuf.Struct; + +public class Query implements Comparable { + + private final UUID id; + private String resource; + private String name; + private Long userId; + private Struct query; + + public Query(UUID id, String resource, String name, Long userId, Struct query) { + this.id = id; + this.resource = resource; + this.name = name; + this.userId = userId; + this.query = query; + } + + public Query(Tuple tuple) { + id = tuple.getColumn(CNAME_ID); + resource = tuple.getColumn(CNAME_RESOURCE); + name = tuple.getColumn(CNAME_NAME); + userId = tuple.getColumn(CNAME_USER_ID); + query = tuple.getColumn(CNAME_QUERY); + } + + public UUID getId() { + return id; + } + + public String getResource() { + return resource; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Struct getQuery() { + return query; + } + + public void setQuery(Struct query) { + this.query = query; + } + + public Tuple toTuple() { + var tuple = new Tuple(); + tuple.addColumn(CNAME_ID, DataType.UUID, id); + tuple.addColumn(CNAME_RESOURCE, resource); + tuple.addColumn(CNAME_NAME, name); + tuple.addColumn(CNAME_USER_ID, DataType.LONG, userId); // Nullable + tuple.addColumn(CNAME_QUERY, STRUCT_TYPE, query); + return tuple; + } + + @Override + public int compareTo(Query other) { + return name.compareToIgnoreCase(other.name); + } + + @Override + public String toString() { + return name; + } +} diff --git a/yamcs-web/src/main/java/org/yamcs/web/db/QueryDb.java b/yamcs-web/src/main/java/org/yamcs/web/db/QueryDb.java new file mode 100644 index 00000000000..ab5f62eb76d --- /dev/null +++ b/yamcs-web/src/main/java/org/yamcs/web/db/QueryDb.java @@ -0,0 +1,161 @@ +package org.yamcs.web.db; + +import static org.yamcs.yarch.query.Query.createStream; +import static org.yamcs.yarch.query.Query.createTable; +import static org.yamcs.yarch.query.Query.deleteFromTable; +import static org.yamcs.yarch.query.Query.selectStream; +import static org.yamcs.yarch.query.Query.selectTable; +import static org.yamcs.yarch.query.Query.upsertIntoTable; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.yamcs.YamcsServerInstance; +import org.yamcs.logging.Log; +import org.yamcs.management.ManagementListener; +import org.yamcs.management.ManagementService; +import org.yamcs.utils.parser.ParseException; +import org.yamcs.yarch.DataType; +import org.yamcs.yarch.Stream; +import org.yamcs.yarch.Tuple; +import org.yamcs.yarch.TupleDefinition; +import org.yamcs.yarch.YarchDatabase; +import org.yamcs.yarch.YarchDatabaseInstance; +import org.yamcs.yarch.streamsql.StreamSqlException; + +import com.google.protobuf.Struct; + +public class QueryDb { + + public static final String TABLE_NAME = "web_query"; + private static final TupleDefinition TDEF = new TupleDefinition(); + public static final String CNAME_ID = "id"; + public static final String CNAME_RESOURCE = "resource"; + public static final String CNAME_NAME = "name"; + public static final String CNAME_USER_ID = "user_id"; + public static final String CNAME_QUERY = "query"; + public static final DataType STRUCT_TYPE = DataType.protobuf(Struct.class.getName()); + private static ConcurrentMap dbs = new ConcurrentHashMap<>(); + static { + TDEF.addColumn(CNAME_ID, DataType.UUID); + TDEF.addColumn(CNAME_RESOURCE, DataType.STRING); + TDEF.addColumn(CNAME_NAME, DataType.STRING); + TDEF.addColumn(CNAME_USER_ID, DataType.LONG); + TDEF.addColumn(CNAME_QUERY, STRUCT_TYPE); + + ManagementService.getInstance().addManagementListener(new ManagementListener() { + @Override + public void instanceStateChanged(YamcsServerInstance ysi) { + switch (ysi.state()) { + case OFFLINE: + case FAILED: + dbs.remove(ysi.getName()); + break; + default: + // Ignore + } + } + }); + } + + private Log log; + private YarchDatabaseInstance ydb; + private Stream tableStream; + private ReadWriteLock rwlock = new ReentrantReadWriteLock(); + + private QueryDb(String yamcsInstance) throws StreamSqlException, ParseException { + log = new Log(QueryDb.class, yamcsInstance); + ydb = YarchDatabase.getInstance(yamcsInstance); + + var streamName = TABLE_NAME + "_in"; + if (ydb.getTable(TABLE_NAME) == null) { + var q = createTable(TABLE_NAME, TDEF) + .primaryKey(CNAME_ID); + ydb.execute(q.toStatement()); + } + if (ydb.getStream(streamName) == null) { + var q = createStream(streamName, TDEF); + ydb.execute(q.toStatement()); + } + + var q = upsertIntoTable(TABLE_NAME) + .query(selectStream(streamName).toSQL()); + ydb.execute(q.toStatement()); + + tableStream = ydb.getStream(streamName); + } + + public Query getById(UUID id) { + rwlock.readLock().lock(); + try { + var q = selectTable(TABLE_NAME).where(CNAME_ID, id); + var r = ydb.executeUnchecked(q.toStatement()); + try { + if (r.hasNext()) { + Tuple tuple = r.next(); + try { + var query = new Query(tuple); + log.trace("Read query from db {}", query); + return query; + } catch (Exception e) { + log.error("Cannot decode tuple {} into query", tuple); + } + } + } finally { + r.close(); + } + return null; + } finally { + rwlock.readLock().unlock(); + } + } + + public void insert(Query query) { + rwlock.writeLock().lock(); + try { + var tuple = query.toTuple(); + log.trace("Adding query: {}", tuple); + tableStream.emitTuple(tuple); + } finally { + rwlock.writeLock().unlock(); + } + } + + public void update(Query query) { + rwlock.writeLock().lock(); + try { + var tuple = query.toTuple(); + log.trace("Updating query: {}", tuple); + tableStream.emitTuple(tuple); + } finally { + rwlock.writeLock().unlock(); + } + } + + public void delete(UUID queryId) { + rwlock.writeLock().lock(); + try { + var query = deleteFromTable(TABLE_NAME).where(CNAME_ID, queryId); + var result = ydb.executeUnchecked(query.toStatement()); + result.close(); + } finally { + rwlock.writeLock().unlock(); + } + } + + /** + * Retrieve a {@link QueryDb} for the given Yamcs instance. + */ + public static QueryDb getInstance(String yamcsInstance) { + return dbs.computeIfAbsent(yamcsInstance, x -> { + try { + return new QueryDb(yamcsInstance); + } catch (StreamSqlException | ParseException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/yamcs-web/src/main/proto/yamcs/protobuf/web/web.proto b/yamcs-web/src/main/proto/yamcs/protobuf/web/web.proto index 02bd21136bf..360664ac4e3 100644 --- a/yamcs-web/src/main/proto/yamcs/protobuf/web/web.proto +++ b/yamcs-web/src/main/proto/yamcs/protobuf/web/web.proto @@ -6,6 +6,9 @@ option java_package = "org.yamcs.web.api"; option java_outer_classname = "WebProto"; option java_multiple_files = true; +import "google/protobuf/empty.proto"; +import "google/protobuf/struct.proto"; + import "yamcs/api/annotations.proto"; service WebApi { @@ -16,6 +19,50 @@ service WebApi { get: "/api/web/instance-config/{instance}" }; } + + // List queries for a resource + rpc ListQueries(ListQueriesRequest) returns (ListQueriesResponse) { + option (yamcs.api.route) = { + get: "/api/web/queries/{instance}/{resource}" + }; + } + + // Create a query on a target resource + rpc CreateQuery(CreateQueryRequest) returns (QueryInfo) { + option (yamcs.api.route) = { + post: "/api/web/queries/{instance}/{resource}" + body: "*" + }; + } + + // Update a query + rpc UpdateQuery(UpdateQueryRequest) returns (QueryInfo) { + option (yamcs.api.route) = { + patch: "/api/web/queries/{instance}/{resource}/{id}" + body: "*" + }; + } + + // Delete a query + rpc DeleteQuery(DeleteQueryRequest) returns (google.protobuf.Empty) { + option (yamcs.api.route) = { + delete: "/api/web/queries/{instance}/{resource}/{id}" + }; + } + + // Parses a resource filter query + // + // This operation was added to assist yamcs-web in using the + // server-side implementation to parse query filters, while the + // client-side implementation is not sufficiently complete. + // + // It is modeled as a WebSocket, to avoid an abundance of ignorable + // requests and 404 warnings in the server log. + rpc ParseFilter(stream ParseFilterRequest) returns (stream ParseFilterData) { + option (yamcs.api.websocket) = { + topic: "web.parse-filter" + }; + } } message GetInstanceConfigurationRequest { @@ -30,3 +77,96 @@ message InstanceConfiguration { // Bucket where to find stacks optional string stackBucket = 2; } + +message ListQueriesRequest { + // Yamcs instance name + optional string instance = 1; + + // Resource identifier + optional string resource = 2; +} + +message ListQueriesResponse { + + // Matching queries + repeated QueryInfo queries = 1; +} + +message CreateQueryRequest { + // Yamcs instance name + optional string instance = 1; + + // Resource identifier + optional string resource = 2; + + // Query name + optional string name = 3; + + // If true, everyone can use query, else this + // query is owned by the requesting user + optional bool shared = 4; + + // Query definition + optional google.protobuf.Struct query = 5; +} + +message UpdateQueryRequest { + // Yamcs instance name + optional string instance = 1; + + // Resource identifier + optional string resource = 2; + + // Query identifier + optional string id = 3; + + // Query name + optional string name = 4; + + // If true, everyone can use query, else this + // query is owned by the requesting user + optional bool shared = 5; + + // Query definition + optional google.protobuf.Struct query = 6; +} + +message DeleteQueryRequest { + // Yamcs instance name + optional string instance = 1; + + // Resource identifier + optional string resource = 2; + + // Query identifier + optional string id = 3; +} + +message QueryInfo { + + // Query identifier + optional string id = 1; + + // Query name + optional string name = 2; + + // If true, everyone can use query, else this + // query is owned by the requesting user + optional bool shared = 3; + + // Query definition + optional google.protobuf.Struct query = 4; +} + +message ParseFilterRequest { + optional string resource = 1; + optional string filter = 2; +} + +message ParseFilterData { + optional string errorMessage = 1; + optional int32 beginLine = 2; + optional int32 beginColumn = 3; + optional int32 endLine = 4; + optional int32 endColumn = 5; +} diff --git a/yamcs-web/src/main/webapp/package-lock.json b/yamcs-web/src/main/webapp/package-lock.json index cd704c83ab0..7d9ea11688b 100644 --- a/yamcs-web/src/main/webapp/package-lock.json +++ b/yamcs-web/src/main/webapp/package-lock.json @@ -60,13 +60,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.3.tgz", - "integrity": "sha512-WQ2AmkUKy1bqrDlNfozW8+VT2Tv/Fdmu4GIXps3ytZANyAKiIvTzmmql2cRCXXraa9FNMjLWNvz+qolDxWVdYQ==", + "version": "0.1802.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.4.tgz", + "integrity": "sha512-VH7AwGng1zuWPTJoH1IgHYeNhqZIgzlwDx39JPmArZAW/WZHDILWB7ipbTNw0R4U4VncrXJqDmMVex7NdHP6sg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.3", + "@angular-devkit/core": "18.2.4", "rxjs": "7.8.1" }, "engines": { @@ -76,17 +76,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.3.tgz", - "integrity": "sha512-uUQba0SIskKORHcPayt7LpqPRKD//48EW92SgGHEArn2KklM+FSYBOA9OtrJeZ/UAcoJpdLDtvyY4+S7oFzomg==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.4.tgz", + "integrity": "sha512-zLDstS95Yb7iBA1ZCYe3LsOLpIhr0ZC3sZ03PhVvAGbVRGSbQNnhQRZLKMk+LDhYJiG+eNFQGLfU3RfZrGds7A==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.3", - "@angular-devkit/build-webpack": "0.1802.3", - "@angular-devkit/core": "18.2.3", - "@angular/build": "18.2.3", + "@angular-devkit/architect": "0.1802.4", + "@angular-devkit/build-webpack": "0.1802.4", + "@angular-devkit/core": "18.2.4", + "@angular/build": "18.2.4", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -97,7 +97,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.3", + "@ngtools/webpack": "18.2.4", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -212,13 +212,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.3.tgz", - "integrity": "sha512-/Nixv9uAg6v/OPoZa0PB0zi+iezzBkgLrnrJnestny5B536l9WRtsw97RjeQDu+x2BClQsxNe8NL2A7EvjVD6w==", + "version": "0.1802.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.4.tgz", + "integrity": "sha512-juaDoguYccObm2xnzRDRlOtiL7ZyZcSAyiyls6QuO8hoo/h6phdHALJkUhI9+SIhCRQ6eUQtolC7hN3J+FZKnA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/architect": "0.1802.4", "rxjs": "7.8.1" }, "engines": { @@ -232,9 +232,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.3.tgz", - "integrity": "sha512-vbFs+ofNK9OWeMIcFarFjegXVklhtSdLTEFKZ9trDVr8alTJdjI9AiYa6OOUTDAyq0hqYxV26xlCisWAPe7s5w==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.4.tgz", + "integrity": "sha512-svlgZ0vbLrfNJAQE5WePAutcYIyA7C0OfzKSTMsfV2X1I+1blYDaZIu/ocnHqofMHu6ZqdSaaU/p/rieqU8fcA==", "dev": true, "license": "MIT", "dependencies": { @@ -260,13 +260,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.3.tgz", - "integrity": "sha512-N3tRAzBW2yWQhebvc1Ha18XTMSXOQTfr8HNjx7Fasx0Fg1tNyGR612MJNZw6je/PqyItKeUHOhztvFMfCQjRyg==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.4.tgz", + "integrity": "sha512-s2WdUhyLlKj5kOjb6vrvJg9/31KvgyRJGjy7PnzS43tpwF9MLuM3AYhuJsXHPhx+i0nyWn/Jnd8ZLjMzXljSxg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.3", + "@angular-devkit/core": "18.2.4", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -279,9 +279,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.3.tgz", - "integrity": "sha512-rIATopHr83lYR0X05buHeHssq9CGw0I0YPIQcpUTGnlqIpJcQVCf7jCFn4KGZrE9V55hFY3MD4S28njlwCToQQ==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.4.tgz", + "integrity": "sha512-ajjXpLD+SyxbHnmhj2ZvYpXneviOjcBgU9L2UX4OVS0jVWxCNHLhJrcMqtqFHA6U5fPnhPNR9cmnt6tmqri0rA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -290,18 +290,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.3" + "@angular/core": "18.2.4" } }, "node_modules/@angular/build": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.3.tgz", - "integrity": "sha512-USrD2Zvcb1te2dnqhH7JZ5XeJDg/t7fjUHR4f93vvMrnrncwCjLoHbHpz01HCHfcIVRgsYUdAmAi1iG7vpak7w==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.4.tgz", + "integrity": "sha512-GVs7O7wxNMJCkqh6Vv2u9GEArWg9jyEt8Fofd6CJGzxKBYQ4hR5gjzL/lU6kNFiMcioS1wm1f6qtJtgilUO+9A==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/architect": "0.1802.4", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -363,9 +363,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.3.tgz", - "integrity": "sha512-lUcpYTxPZuntJ1FK7V2ugapCGMIhT6TUDjIGgXfS9AxGSSKgwr8HNs6Ze9pcjYC44UhP40sYAZuiaFwmE60A2A==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.4.tgz", + "integrity": "sha512-o+TuxZDqStfkviEkCR05pVyP6R2RIruEs/45Cms76hlsIheMoxRaxir/yrHdh4tZESJJhcO/EVE+aymNIRWAfg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -380,18 +380,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.3.tgz", - "integrity": "sha512-40258vuliH6+p8QSByZe5EcIXSj0iR3PNF6yuusClR/ByToHOnmuPw7WC+AYr0ooozmqlim/EjQe4/037OUB3w==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.4.tgz", + "integrity": "sha512-n+Y2xlgcpTZ+MZmycf2b3ceVvANDJFkDEodobVtyG63WvGOhkZ3aGhT7sHguKpAQwJLicSf8zF2z+v1Yi0DvRw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.3", - "@angular-devkit/core": "18.2.3", - "@angular-devkit/schematics": "18.2.3", + "@angular-devkit/architect": "0.1802.4", + "@angular-devkit/core": "18.2.4", + "@angular-devkit/schematics": "18.2.4", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.3", + "@schematics/angular": "18.2.4", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -414,9 +414,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.3.tgz", - "integrity": "sha512-NFL4yXXImSCH7i1xnHykUjHa9vl9827fGiwSV2mnf7LjSUsyDzFD8/54dNuYN9OY8AUD+PnK0YdNro6cczVyIA==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.4.tgz", + "integrity": "sha512-flUaKhdr8KvtjH0cLC6Rrjirt8GsiFlrmZLZplr784O3Gkei2VBBNFoopgmlEzbVGPiIG5QlFZH9yvah6JPQZw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -425,14 +425,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.3", + "@angular/core": "18.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.3.tgz", - "integrity": "sha512-Il3ljs0j1GaYoqYFdShjUP1ryck5xTOaA8uQuRgqwU0FOwEDfugSAM3Qf7nJx/sgxTM0Lm/Nrdv2u6i1gZWeuQ==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.4.tgz", + "integrity": "sha512-o3ngFr1Bjt7cKOu4DSZJGCUF9YPTxJee97wFon2eNFj6FFNTmnGwAvsnJjHBMmk90fmZLC2/HpPdhYz7WprMZQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -441,7 +441,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.3" + "@angular/core": "18.2.4" }, "peerDependenciesMeta": { "@angular/core": { @@ -450,9 +450,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.3.tgz", - "integrity": "sha512-BcmqYKnkcJTkGjuPztClZNQve7tdI290J5F3iZBx6c7/vaw8EU8EGZtpWYZpgiVn5S6jhcKyc1dLF9ggO9vftg==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.4.tgz", + "integrity": "sha512-BIp5zr+npqSs/4KWPxwKdn7+sjo008ieNOQDlXyQms9BKlxx/gDnj7W2TsxhrkDTYCIHF73fJZ7u2U8Qy4JWfw==", "dev": true, "license": "MIT", "dependencies": { @@ -474,14 +474,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.3", + "@angular/compiler": "18.2.4", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.3.tgz", - "integrity": "sha512-VGhMJxj7d0rYpqVfQrcGRB7EE/BCziotft/I/YPl6bOMPSAvMukG7DXQuJdYpNrr62ks78mlzHlZX/cdmB9Prw==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.4.tgz", + "integrity": "sha512-ulYmYpI/ZVQ5BL38rBy4DS/9wgGWmVD9Uo6tcrLqCzt1G1G2nKwseZv009536pHfk6dj2HdPSkpcerhWh57DWw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -495,9 +495,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.3.tgz", - "integrity": "sha512-+OBaAH0e8hue9eyLnbgpxg1/X9fps6bwXECfJ0nL5BDPU5itZ428YJbEnj5bTx0hEbqfTRiV4LgexdI+D9eOpw==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.4.tgz", + "integrity": "sha512-rlLhReauUz6jhLCEkGabLqqF6xLaTfvxafuj2ojzcqoKGSZcXDIM/UOSoWX756B8NtrpsuglpGBZjZlsrAZGsg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -506,32 +506,32 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.3", - "@angular/core": "18.2.3", - "@angular/platform-browser": "18.2.3", + "@angular/common": "18.2.4", + "@angular/core": "18.2.4", + "@angular/platform-browser": "18.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.3.tgz", - "integrity": "sha512-bTZ1O7s0uJqKdd9ImCleRS9Wg6yVy2ZXchnS5ap2gYJx51MJgwOM/fL6is0OsovtZG/UJaKK5FeEqUUxNqZJVA==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.4.tgz", + "integrity": "sha512-Keg6n8u8xHLhRDTmx4hUqh1AtVFUt8hDxPMYSUu64czjOT5Dnh8XsgKagu563NEjxbDaCzttPuO+y3DlcaDZoQ==", "license": "MIT", "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0" } }, "node_modules/@angular/material": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.3.tgz", - "integrity": "sha512-JFfvXaMHMhskncaxxus4sDvie9VYdMkfYgfinkLXpZlPFyn1IzjDw0c1BcrcsuD7UxQVZ/v5tucCgq1FQfGRpA==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.4.tgz", + "integrity": "sha512-F09145mI/EAHY9ngdnQTo3pFRmUoU/50i6cmddtL4cse0WidatoodQr0gZCksxhmpJgRy5mTcjh/LU2hShOgcA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.3", + "@angular/cdk": "18.2.4", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -540,9 +540,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.3.tgz", - "integrity": "sha512-M2ob4zN7tAcL2mx7U6KnZNqNFPFl9MlPBE0FrjQjIzAjU0wSYPIJXmaPu9aMUp9niyo+He5iX98I+URi2Yc99g==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz", + "integrity": "sha512-ddzq5MyPvFyTv0kUe8U9fbhE1nZtLYBCFKDqICrzHXiVRAdqLv6qtE5PtbURrdspSy1u/YEGV4LdkNJK3UgnZQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -551,9 +551,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.3", - "@angular/common": "18.2.3", - "@angular/core": "18.2.3" + "@angular/animations": "18.2.4", + "@angular/common": "18.2.4", + "@angular/core": "18.2.4" }, "peerDependenciesMeta": { "@angular/animations": { @@ -562,9 +562,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.3.tgz", - "integrity": "sha512-nWi9ZxN4KpbJkttIckFO1PCoW0+gb/18xFO+JWyLBAtcbsudj/Mv0P/fdOaSfQdLkPhZfORr3ZcfiTkhmuGyEg==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.4.tgz", + "integrity": "sha512-0nA04zJueGzdnl37TJ7guDCrzxYS4fjqgvYKiOpFktpMHPuNrBlAQo5YA7u20HGFG3i47PQh7hEWhQaqcXXpQw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -573,16 +573,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.3", - "@angular/compiler": "18.2.3", - "@angular/core": "18.2.3", - "@angular/platform-browser": "18.2.3" + "@angular/common": "18.2.4", + "@angular/compiler": "18.2.4", + "@angular/core": "18.2.4", + "@angular/platform-browser": "18.2.4" } }, "node_modules/@angular/router": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.3.tgz", - "integrity": "sha512-fvD9eSDIiIbeYoUokoWkXzu7/ZaxlzKPUHFqX1JuKuH5ciQDeT/d7lp4mj31Bxammhohzi3+z12THJYsCkj/iQ==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.4.tgz", + "integrity": "sha512-kYNHD3K1Xou2PRMqbG2tVahtMobgDlhwHdMB7G5oFHg6K13gQ2TmopF1U5A2wYtIMdsC+AkVGIJEOxQN8fmgcA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -591,16 +591,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.3", - "@angular/core": "18.2.3", - "@angular/platform-browser": "18.2.3", + "@angular/common": "18.2.4", + "@angular/core": "18.2.4", + "@angular/platform-browser": "18.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.3.tgz", - "integrity": "sha512-KplaBYhhwsM3gPeOImfDGhAknN+BIcZJkHl8YRnhoUEFHsTZ8LTV02C4LWQL3YTu3pK+uj/lPMKi1CA37cXQ8g==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.4.tgz", + "integrity": "sha512-yvdHWtD9m70JE9kPapu819f7g0rsBKvIs1FebS7/fvt4Cj7B6+0xSCnoiQPDE5hpRl8qSk/8eikFcGVM/D8F8g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -612,8 +612,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.3", - "@angular/core": "18.2.3" + "@angular/common": "18.2.4", + "@angular/core": "18.2.4" } }, "node_modules/@babel/code-frame": { @@ -3746,9 +3746,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.3.tgz", - "integrity": "sha512-DDuBHcu23qckt43SexBJaPEIeMc/HKaFOidILZM9D4gU4C9VroMActdR218dvQ802QfL0S46t5Ykz8ENprIfjA==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.4.tgz", + "integrity": "sha512-JVDRexu3q7lg2oqJG36RtX7cqTheoZRwg2HhMV8hYXUDL0fyOrv2galwTCgXrx7vAjlx45L2uR2kuWbgW0VVcQ==", "dev": true, "license": "MIT", "engines": { @@ -4349,9 +4349,9 @@ ] }, "node_modules/@rollup/wasm-node": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.21.2.tgz", - "integrity": "sha512-AJCfdXkpe5EX+jfWOMYuFl3ZomTQyfx4V4geRmChdTwAo05FdpnobwqtYn0mo7Mf1qVN7mniI7kdG98vKDVd2g==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.21.3.tgz", + "integrity": "sha512-uZFl4GXMgyllfuKjY/zlXxTxDs+G/LB7snVENskpJt7IIXw6cD1yqi3eBeGM8NxE9kuxrNB0Qr1QLNtDYTlqeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4369,14 +4369,14 @@ } }, "node_modules/@schematics/angular": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.3.tgz", - "integrity": "sha512-whSON70z9HYb4WboVXmPFE/RLKJJQLWNzNcUyi8OSDZkQbJnYgPp0///n738m26Y/XeJDv11q1gESy+Zl2AdUw==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.4.tgz", + "integrity": "sha512-GxrPv4eOPrjMKoAVhch9eprW8H/DFhBy5Zgp7CgGui9NprYkkubxw/yyo11WfR5CFZ/q5AfsjV76dPCkhLwLmA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.3", - "@angular-devkit/schematics": "18.2.3", + "@angular-devkit/core": "18.2.4", + "@angular-devkit/schematics": "18.2.4", "jsonc-parser": "3.3.1" }, "engines": { @@ -5375,9 +5375,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -5389,7 +5389,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5633,9 +5633,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001659", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz", - "integrity": "sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "dev": true, "funding": [ { @@ -6654,9 +6654,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.18", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz", - "integrity": "sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==", + "version": "1.5.20", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.20.tgz", + "integrity": "sha512-74mdl6Fs1HHzK9SUX4CKFxAtAe3nUns48y79TskHNAG6fGOlLfyKA4j855x+0b5u8rWJIrlaG9tcTPstMlwjIw==", "dev": true, "license": "ISC" }, @@ -6678,9 +6678,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -7049,38 +7049,38 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -7198,14 +7198,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -9075,11 +9075,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10575,9 +10578,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true, "license": "MIT" }, @@ -10900,13 +10903,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -11038,9 +11041,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "license": "MIT", "dependencies": { @@ -11522,9 +11525,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -11563,6 +11566,16 @@ "dev": true, "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -11660,16 +11673,16 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -12589,9 +12602,9 @@ "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "license": "MIT", "engines": { @@ -12613,9 +12626,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "license": "MIT", "engines": { diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/.gitignore b/yamcs-web/src/main/webapp/projects/lezer-filter/.gitignore new file mode 100644 index 00000000000..7f4ccdf7b4e --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/.gitignore @@ -0,0 +1,6 @@ +/dist/ +/node_modules/ + +# Generated, used only while testing generator +src/parser.js +src/parser.terms.js diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/package-lock.json b/yamcs-web/src/main/webapp/projects/lezer-filter/package-lock.json new file mode 100644 index 00000000000..495fa5fd3fd --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/package-lock.json @@ -0,0 +1,1415 @@ +{ + "name": "@yamcs/lezer-filter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yamcs/lezer-filter", + "version": "1.0.0", + "devDependencies": { + "@lezer/generator": "^1.7.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "mocha": "^10.7.3", + "rollup": "^4.21.2" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/generator": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.1.tgz", + "integrity": "sha512-MgPJN9Si+ccxzXl3OAmCeZuUKw4XiPl4y664FX/hnnyG9CTqUPq65N3/VGPA2jD23D7QgMTtNqflta+cPN+5mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.1.0", + "@lezer/lr": "^1.3.0" + }, + "bin": { + "lezer-generator": "src/lezer-generator.cjs" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", + "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", + "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", + "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", + "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", + "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", + "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", + "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", + "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", + "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", + "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", + "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", + "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", + "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", + "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/package.json b/yamcs-web/src/main/webapp/projects/lezer-filter/package.json new file mode 100644 index 00000000000..61b4a1af684 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/package.json @@ -0,0 +1,19 @@ +{ + "name": "@yamcs/lezer-filter", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "lezer-generator src/filter.grammar -o src/parser && rollup -c", + "build-debug": "lezer-generator src/filter.grammar --names -o src/parser && rollup -c", + "test": "npm run build && mocha test/test-filter.js", + "deploy": "npm run build && cp src/*.js ../webapp-sdk/src/lib/components/filter/" + }, + "type": "module", + "sideEffects": false, + "devDependencies": { + "@lezer/generator": "^1.7.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "mocha": "^10.7.3", + "rollup": "^4.21.2" + } +} diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/rollup.config.js b/yamcs-web/src/main/webapp/projects/lezer-filter/rollup.config.js new file mode 100644 index 00000000000..7f5619d8fcf --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/rollup.config.js @@ -0,0 +1,15 @@ +import { nodeResolve } from "@rollup/plugin-node-resolve"; + +export default { + input: "./src/parser.js", + output: [ + { format: "cjs", file: "./dist/index.cjs" }, + { format: "es", file: "./dist/index.js" } + ], + external(id) { + return !/^[\.\/]/.test(id) + }, + plugins: [ + nodeResolve() + ] +} diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/src/filter.grammar b/yamcs-web/src/main/webapp/projects/lezer-filter/src/filter.grammar new file mode 100644 index 00000000000..addec908588 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/src/filter.grammar @@ -0,0 +1,58 @@ +@top Filter {(value | Comparison | LogicOp)* } + +value { + True + | False + | Null + | Number + | (Minus? String) + | (Minus? Text) +} + +String[isolate] { string } +Text[isolate] { text } + +Comparison { Comparable CompareOp value } +Comparable[isolate] { text | string } + +CompareOp { "=" | "!=" | "<" | ">" | "<=" | ">=" | "=~" | "!~" | ":" } +LogicOp { "AND" | "OR" | "NOT" } + + +@tokens { + True { "true" } + False { "false" } + Null { "null" } + + Number { '-'? int frac? exp? } + int { '0' | $[1-9] @digit* } + frac { '.' @digit+ } + exp { $[eE] $[+\-]? @digit+ } + + string { '"' char* '"' } + text { $[a-zA-Z_$] $[a-zA-Z0-9_$\.]* } + char { $[\u{20}\u{21}\u{23}-\u{5b}\u{5d}-\u{10ffff}] | "\\" esc } + esc { $["\\\/bfnrt] } + + LineComment { "--" ![\n]* } + Minus { "-" } + + whitespace { $[ \n\r\t] } + + "(" ")" + + @precedence { True, text } + @precedence { False, text } + @precedence { Null, text } + @precedence { LineComment, Minus } + @precedence { Number, Minus } + @precedence { "AND", text } + @precedence { "OR", text } + @precedence { "NOT", text } +} + +@skip { whitespace | LineComment | "(" | ")" } + +@external propSource filterHighlighting from "./highlight" + +@detectDelim diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/src/highlight.js b/yamcs-web/src/main/webapp/projects/lezer-filter/src/highlight.js new file mode 100644 index 00000000000..41c85160f62 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/src/highlight.js @@ -0,0 +1,15 @@ +import { styleTags, tags as t } from "@lezer/highlight"; + +export const filterHighlighting = styleTags({ + String: t.string, + Text: t.literal, + LineComment: t.lineComment, + CompareOp: t.compareOperator, + Comparable: t.propertyName, + LogicOp: t.logicOperator, + Number: t.number, + "True False": t.bool, + Minus: t.operator, + Null: t.null, + "( )": t.paren +}) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/comments.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/comments.txt new file mode 100644 index 00000000000..a776b4b09f4 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/comments.txt @@ -0,0 +1,8 @@ +# Line comment + +-- Some comment +foo = 123 + +==> + +Filter(LineComment,Comparison(Comparable,CompareOp,Number)) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/comparisons.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/comparisons.txt new file mode 100644 index 00000000000..ebd660fdec9 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/comparisons.txt @@ -0,0 +1,38 @@ +# Text Comparison + +foo = 123 + +==> + +Filter(Comparison(Comparable,CompareOp,Number)) + +# String Comparison + +foo="123" + +==> + +Filter(Comparison(Comparable,CompareOp,String)) + +# Operators + +foo != "123" +foo < "123" +foo <= "123" +foo > "123" +foo >= "123" +foo =~ "123" +foo !~ "123" +foo : "123" + +==> + +Filter( + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String), + Comparison(Comparable,CompareOp,String)) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/literals.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/literals.txt new file mode 100644 index 00000000000..3ec5d0dfbb3 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/literals.txt @@ -0,0 +1,23 @@ +# True + +true + +==> + +Filter(True) + +# False + +false + +==> + +Filter(False) + +# Null + +null + +==> + +Filter(Null) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/logical.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/logical.txt new file mode 100644 index 00000000000..931740de2ac --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/logical.txt @@ -0,0 +1,21 @@ +# Logical operator + +foo = 123 AND bar = "abc" + +==> + +Filter( + Comparison(Comparable,CompareOp,Number), + LogicOp, + Comparison(Comparable,CompareOp,String),) + +# Not a logical operator + +foo = 123 and bar = "abc" + +==> + +Filter( + Comparison(Comparable,CompareOp,Number), + Text, + Comparison(Comparable,CompareOp,String),) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/parens.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/parens.txt new file mode 100644 index 00000000000..b4d1fca4d87 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/parens.txt @@ -0,0 +1,7 @@ +# Single term + +abc OR (def AND ghi) + +==> + +Filter(Text,LogicOp,Text,LogicOp,Text) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/strings.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/strings.txt new file mode 100644 index 00000000000..845215e6424 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/strings.txt @@ -0,0 +1,23 @@ +# Empty String + +"" + +==> + +Filter(String) + +# Non-empty String + +"This is a boring old string" + +==> + +Filter(String) + +# All The Valid One-Character Escapes + +"\"\\\/\b\f\n\rt\t" + +==> + +Filter(String) diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/test-filter.js b/yamcs-web/src/main/webapp/projects/lezer-filter/test/test-filter.js new file mode 100644 index 00000000000..0936b68b620 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/test-filter.js @@ -0,0 +1,17 @@ +import { fileTests } from '@lezer/generator/dist/test' +import { parser } from '../dist/index.js' + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from "url" +let caseDir = path.dirname(fileURLToPath(import.meta.url)) + +for (let file of fs.readdirSync(caseDir)) { + if (file === 'test-filter.js') continue + + let name = /^[^\.]*/.exec(file)[0] + describe(name, () => { + for (let { name, run } of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file)) + it(name, () => run(parser)) + }) +} diff --git a/yamcs-web/src/main/webapp/projects/lezer-filter/test/texts.txt b/yamcs-web/src/main/webapp/projects/lezer-filter/test/texts.txt new file mode 100644 index 00000000000..fbd4073b918 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/lezer-filter/test/texts.txt @@ -0,0 +1,35 @@ +# Single term + +abc + +==> + +Filter(Text) + +# Sequence + +abc +def + +==> + +Filter(Text, Text) + +# Negate text term + +abc +-def + +==> + +Filter(Text, Minus, Text) + + +# Negate string term + +"abc def" +-"def ghi" + +==> + +Filter(String, Minus, String) diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/HttpError.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/HttpError.ts index 49d49fe36cd..4d162324948 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/HttpError.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/HttpError.ts @@ -2,10 +2,17 @@ export class HttpError extends Error { readonly statusCode: number; readonly statusText: string; + readonly detail?: ErrorDetail; - constructor(readonly response: Response, serverMessage?: string) { + constructor(readonly response: Response, serverMessage?: string, detail?: ErrorDetail) { super(serverMessage || response.statusText); this.statusCode = response.status; this.statusText = response.statusText; + this.detail = detail; } } + +export interface ErrorDetail { + [key: string]: any, + "@type": string, +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/WebSocketCall.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/WebSocketCall.ts index 6f1560966d5..fb3e4203c26 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/WebSocketCall.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/WebSocketCall.ts @@ -11,13 +11,16 @@ export class WebSocketCall { private _frameLoss = false; private frameLossListeners = new Set<() => void>(); private replyListeners = new Set<() => void>(); + private messageListeners = new Set<(data: D) => void>(); constructor( private client: WebSocketClient, private requestId: number, private type: string, - private observer: (data: D) => void, - ) { } + observer: (data: D) => void, + ) { + this.messageListeners.add(observer); + } /** * Returns the server-assigned unique call id @@ -61,6 +64,14 @@ export class WebSocketCall { this.replyListeners.delete(replyListener); } + addMessageListener(messageListener: (data: D) => void) { + this.messageListeners.add(messageListener); + } + + removeMessageListener(messageListener: (data: D) => void) { + this.messageListeners.delete(messageListener); + } + sendMessage(options: O) { if (this._id !== undefined) { this.client.sendMessage({ @@ -94,7 +105,7 @@ export class WebSocketCall { this.frameLossListeners.forEach(listener => listener()); } this.seq = msg.seq; - this.observer(msg.data); + this.messageListeners.forEach(l => l(msg.data)); } } diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/YamcsClient.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/YamcsClient.ts index fdb76c6988c..37abf563375 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/YamcsClient.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/YamcsClient.ts @@ -23,6 +23,7 @@ import { AuditRecordsPage, AuthInfo, Clearance, ClearanceSubscription, CompactRo import { Record, Stream, StreamData, StreamEvent, StreamStatisticsSubscription, StreamSubscription, SubscribeStreamRequest, SubscribeStreamStatisticsRequest, Table } from './types/table'; import { SubscribeTimeRequest, Time, TimeSubscription } from './types/time'; import { CreateTimelineBandRequest, CreateTimelineItemRequest, CreateTimelineViewRequest, GetTimelineItemsOptions, TimelineBand, TimelineBandsPage, TimelineItem, TimelineItemsPage, TimelineTagsPage, TimelineView, TimelineViewsPage, UpdateTimelineBandRequest, UpdateTimelineItemRequest, UpdateTimelineViewRequest } from './types/timeline'; +import { CreateQueryRequest, EditQueryRequest, ListQueriesResponse, ParseFilterData, ParseFilterRequest, ParseFilterSubscription, Query } from './types/web'; export default class YamcsClient implements HttpHandler { @@ -685,6 +686,10 @@ export default class YamcsClient implements HttpHandler { return this.webSocketClient!.createLowPrioritySubscription('parameters', options, observer); } + createParseFilterSubscription(options: ParseFilterRequest, observer: (data: ParseFilterData) => void): ParseFilterSubscription { + return this.webSocketClient!.createSubscription('web.parse-filter', options, observer); + } + createStreamStatisticsSubscription(options: SubscribeStreamStatisticsRequest, observer: (event: StreamEvent) => void): StreamStatisticsSubscription { return this.webSocketClient!.createSubscription('stream-stats', options, observer); } @@ -758,8 +763,12 @@ export default class YamcsClient implements HttpHandler { } async getEvents(instance: string, options: GetEventsOptions = {}) { - const url = `${this.apiUrl}/archive/${instance}/events`; - const response = await this.doFetch(url + this.queryString(options)); + const url = `${this.apiUrl}/archive/${instance}/events:list`; + const body = JSON.stringify(options); + const response = await this.doFetch(url, { + body, + method: 'POST', + }); const wrapper = await response.json() as EventsWrapper; return wrapper.events || []; } @@ -788,6 +797,38 @@ export default class YamcsClient implements HttpHandler { return await response.json() as Event; } + async getQueries(instance: string, resource: string) { + const url = `${this.apiUrl}/web/queries/${instance}/${resource}`; + const response = await this.doFetch(url); + const wrapper = await response.json() as ListQueriesResponse; + return wrapper.queries || []; + } + + async createQuery(instance: string, resource: string, options: CreateQueryRequest) { + const body = JSON.stringify(options); + const response = await this.doFetch(`${this.apiUrl}/web/queries/${instance}/${resource}`, { + body, + method: 'POST', + }); + return await response.json() as Query; + } + + async editQuery(instance: string, resource: string, queryId: string, options: EditQueryRequest) { + const body = JSON.stringify(options); + const url = `${this.apiUrl}/web/queries/${instance}/${resource}/${queryId}`; + return await this.doFetch(url, { + body, + method: 'PATCH', + }); + } + + async deleteQuery(instance: string, resource: string, queryId: string) { + const url = `${this.apiUrl}/queries/${instance}/${resource}/${queryId}`; + return await this.doFetch(url, { + method: 'DELETE', + }); + } + getCommandsDownloadURL(instance: string, options: DownloadCommandsOptions = {}) { const url = `${this.apiUrl}/archive/${instance}:exportCommands`; return url + this.queryString(options); @@ -1516,7 +1557,11 @@ export default class YamcsClient implements HttpHandler { } else { return new Promise((resolve, reject) => { response.json().then(json => { - reject(new HttpError(response, json['msg'])); + if (json.hasOwnProperty("detail")) { + reject(new HttpError(response, json['msg'], json['detail'])); + } else { + reject(new HttpError(response, json['msg'])); + } }).catch(err => { console.error('Failure while handling server error', err); reject(new HttpError(response)); diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/index.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/index.ts index 86e8f36b885..ee3158152f8 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/index.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/index.ts @@ -4,9 +4,6 @@ export * from './HttpHandler'; export * from './HttpInterceptor'; export * from './SessionListener'; export * from './StorageClient'; -export * from './WebSocketCall'; -export * from './WebSocketClient'; -export { default as YamcsClient } from './YamcsClient'; export * from './types/activities'; export * from './types/alarms'; export * from './types/commandHistory'; @@ -25,4 +22,7 @@ export * from './types/system'; export * from './types/table'; export * from './types/time'; export * from './types/timeline'; - +export * from './types/web'; +export * from './WebSocketCall'; +export * from './WebSocketClient'; +export { default as YamcsClient } from './YamcsClient'; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/events.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/events.ts index f073950001b..a820de65710 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/events.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/events.ts @@ -2,6 +2,7 @@ import { WebSocketCall } from '../WebSocketCall'; export interface SubscribeEventsRequest { instance: string; + filter?: string; } export type EventSeverity = @@ -38,12 +39,11 @@ export interface GetEventsOptions { */ stop?: string; /** - * Search string + * Filter query */ - q?: string; + filter?: string; severity?: EventSeverity; source?: string | string[]; - pos?: number; limit?: number; order?: 'asc' | 'desc'; } @@ -58,9 +58,9 @@ export interface DownloadEventsOptions { */ stop?: string; /** - * Search string + * Filter query */ - q?: string; + filter?: string; severity?: EventSeverity; source?: string | string[]; delimiter?: 'COMMA' | 'SEMICOLON' | 'TAB'; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/web.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/web.ts new file mode 100644 index 00000000000..6f3565c9a38 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/client/types/web.ts @@ -0,0 +1,39 @@ +import { WebSocketCall } from '../WebSocketCall'; + +export interface CreateQueryRequest { + name: string; + query: { [key: string]: any; }; + shared: boolean; +} + +export interface Query { + id: string; + name: string; + shared: boolean; + query: { [key: string]: any; }; +} + +export interface ListQueriesResponse { + queries?: Query[]; +} + +export interface EditQueryRequest { + name: string; + shared: boolean; + query: { [key: string]: any; }; +} + +export interface ParseFilterRequest { + resource: string; + filter: string; +} + +export interface ParseFilterData { + errorMessage?: string; + beginLine?: number; + beginColumn?: number; + endLine?: number; + endColumn?: number; +} + +export type ParseFilterSubscription = WebSocketCall; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html index 89cc543acd7..00ea3ebf0b8 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html @@ -1,5 +1,9 @@ - diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts index a77b2078c84..ad7671ad14a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts @@ -24,7 +24,7 @@ export interface YaColumnInfo { MatMenuItem, MatIcon, MatMenuTrigger -], + ], }) export class YaColumnChooser implements OnInit { @@ -34,6 +34,12 @@ export class YaColumnChooser implements OnInit { @Input() preferenceKey: string; + @Input() + icon?: string; + + @Input() + text = false; + displayedColumns$ = new BehaviorSubject([]); constructor(private preferenceStore: PreferenceStore) { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/FilterErrorMark.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/FilterErrorMark.ts new file mode 100644 index 00000000000..1bcdb712c0a --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/FilterErrorMark.ts @@ -0,0 +1,6 @@ +export interface FilterErrorMark { + beginLine: number; + beginColumn: number; + endLine: number; + endColumn: number; +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/cmSetup.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/cmSetup.ts new file mode 100644 index 00000000000..eaacb432f17 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/cmSetup.ts @@ -0,0 +1,178 @@ +import { autocompletion, closeBrackets, closeBracketsKeymap, Completion, CompletionContext, completionKeymap } from '@codemirror/autocomplete'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { + bracketMatching, + HighlightStyle, + indentOnInput, + syntaxHighlighting +} from '@codemirror/language'; +import { lintKeymap } from '@codemirror/lint'; +import { highlightSelectionMatches } from '@codemirror/search'; +import { EditorState, Extension } from '@codemirror/state'; +import { + crosshairCursor, + drawSelection, + dropCursor, + highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, + keymap, + lineNumbers, + placeholder, + rectangularSelection +} from '@codemirror/view'; +import { tags } from '@lezer/highlight'; +import { EditorView } from 'codemirror'; +import { StyleSpec } from 'style-mod'; + +const highlightStyle = HighlightStyle.define([ + { tag: tags.compareOperator, color: 'rgb(95, 99, 104)' }, + { tag: tags.lineComment, color: 'rgb(176, 96, 0)' }, + { tag: tags.literal, color: 'rgb(0, 0, 0)' }, + { tag: tags.logicOperator, color: 'rgb(19, 115, 51)' }, + { tag: tags.operator, color: 'rgb(217, 48, 37)' }, + { tag: tags.propertyName, color: 'rgb(118, 39, 187)' }, + { tag: tags.string, color: 'rgb(201, 39, 134)' }, +]); + +const multilineTheme = EditorView.theme({ + '&': { + width: '100%', + height: '100%', + fontSize: '12px', + fontWeight: 'normal', + letterSpacing: 'normal', + }, + '.cm-content, .cm-gutter': { + minHeight: '100px', + }, + '.cm-scroller': { + overflow: 'auto', + fontFamily: "'Roboto Mono', monospace", + }, + '&.cm-focused': { + outline: 'none', + }, + ".cm-underline": { + textDecoration: 'underline 1px red', + textDecorationStyle: 'wavy', + }, +}, { dark: false }); + +function createOnelineTheme(paddingLeft: string | undefined) { + const styles: { [key: string]: StyleSpec; } = { + '&': { + width: '100%', + height: '100%', + fontSize: '12px', + fontWeight: 'normal', + letterSpacing: 'normal', + }, + '.cm-content': { + padding: 0, + }, + '.cm-scroller': { + overflow: 'auto', + fontFamily: 'Roboto, sans-serif', + backgroundColor: '#fff', + lineHeight: '22px', // 24px - 2px top + bottom border + }, + '&.cm-focused': { + outline: 'none', + }, + '.cm-underline': { + textDecoration: 'underline 1px red', + textDecorationStyle: 'wavy', + }, + }; + if (paddingLeft) { + styles['.cm-line'] = { + paddingLeft, + }; + } + return EditorView.theme(styles, { dark: false }); +} + +export interface CodeMirrorConfiguration { + oneline?: boolean; + placeholder?: string; + paddingLeft?: string; + onEnter?: (view: EditorView) => void; + completions?: Completion[]; +} + +export function provideCodeMirrorSetup(options?: CodeMirrorConfiguration): Extension { + function provideCompletions(context: CompletionContext) { + const before = context.matchBefore(/\w+/); + // If completion wasn't explicitly started and there + // is no word before the cursor, don't open completions. + if (!context.explicit && !before) { + return null; + } + return { + from: before ? before.from : context.pos, + options: options?.completions || [], + validFor: /^\w*$/ + }; + } + + const extensions: Extension[] = [ + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + indentOnInput(), + syntaxHighlighting(highlightStyle), + bracketMatching(), + closeBrackets(), + autocompletion({ + override: [provideCompletions], + }), + highlightSelectionMatches(), + ]; + + if (options?.oneline) { + + if (options.onEnter) { + // Important to have this in the extension array + // before any other key mappings (higher priority) + extensions.push(keymap.of([{ + key: 'Enter', + run: view => { + options.onEnter!(view); + return true; + }, + preventDefault: true + }])); + } + + const theme = createOnelineTheme(options?.paddingLeft); + extensions.push(theme); + } else { + extensions.push(...[ + multilineTheme, + lineNumbers(), + highlightActiveLineGutter(), + EditorState.allowMultipleSelections.of(true), + rectangularSelection(), + crosshairCursor(), + highlightActiveLine(), + EditorView.lineWrapping, + ]); + } + + extensions.push( + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...historyKeymap, + ...completionKeymap, + ...lintKeymap, + ])); + + if (options?.placeholder) { + extensions.push(placeholder(options.placeholder)); + } + + return extensions; +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.css new file mode 100644 index 00000000000..6b122920c6a --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.css @@ -0,0 +1,35 @@ +:host { + display: block; + position: relative; +} + +.editor-container { + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.icon { + position: absolute; + top: 0; + left: 0; +} + +.icon .material-icons { + color: darkgrey; + padding: 4px; +} + +.clear { + position: absolute; + top: 0; + right: 0; +} + +.clear .material-icons { + cursor: pointer; + color: darkgrey; + padding: 4px; +} + +.clear:hover .material-icons { + color: black; +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.html new file mode 100644 index 00000000000..cba9911b8d8 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.html @@ -0,0 +1,13 @@ +

+ +@if (icon(); as icon) { +
+ {{ icon }} +
+} + +@if (showClear()) { +
+ close +
+} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.ts new file mode 100644 index 00000000000..2ded77fe2e9 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-input.component.ts @@ -0,0 +1,172 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, effect, ElementRef, forwardRef, input, OnDestroy, output, signal, viewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatIcon } from '@angular/material/icon'; +import { Completion } from '@codemirror/autocomplete'; +import { EditorState, StateEffect, StateEffectType, StateField } from '@codemirror/state'; +import { Decoration, DecorationSet } from "@codemirror/view"; +import { EditorView } from 'codemirror'; +import { provideCodeMirrorSetup } from './cmSetup'; +import { FilterErrorMark } from './FilterErrorMark'; +import { filter } from './lang-filter'; + +@Component({ + standalone: true, + selector: 'ya-filter-input', + templateUrl: './filter-input.component.html', + styleUrl: './filter-input.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => YaFilterInput), + multi: true, + }], + imports: [ + MatIcon, + ], +}) +export class YaFilterInput implements ControlValueAccessor, AfterViewInit, OnDestroy { + + completions = input(); + errorMark = input(); + placeholder = input(); + icon = input(); + + onEnter = output(); + + private editorContainerRef = viewChild.required>('editorContainer'); + private editorView: EditorView | null = null; + private underlineDecoration: Decoration; + private addUnderlineEffect: StateEffectType<{ from: number; to: number; }>; + private removeUnderlineEffect: StateEffectType; + + showClear = signal(false); + + private onChange = (_: string | null) => { }; + + // Internal value, for when a value is received before CM init + private initialDocString: string | undefined; + + constructor() { + effect(() => { + const errorMark = this.errorMark(); + + // Remove any old effect, before adding a new one + this.editorView!.dispatch({ + effects: this.removeUnderlineEffect.of(null), + }); + + if (errorMark) { + const { doc } = this.editorView!.state; + const { beginLine, beginColumn, endLine, endColumn } = errorMark; + + const beginOffset = doc.line(beginLine).from + (beginColumn - 1); + const endOffset = doc.line(endLine).from + endColumn; + this.editorView!.dispatch({ + effects: this.addUnderlineEffect.of(this.underlineDecoration.range(beginOffset, endOffset)), + }); + } + }); + } + + writeValue(value: any): void { + this.initialDocString = value || undefined; + this.editorView?.dispatch({ + changes: { + from: 0, + to: this.editorView.state.doc.length, + insert: this.initialDocString, + }, + }); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngAfterViewInit(): void { + const targetEl = this.editorContainerRef().nativeElement; + this.initializeEditor(targetEl); + } + + focus() { + this.editorView?.focus(); + } + + clearInput() { + this.writeValue(null); + this.focus(); + } + + private initializeEditor(targetEl: HTMLDivElement) { + this.addUnderlineEffect = StateEffect.define({ + map: ({ from, to }, change) => ({ from: change.mapPos(from), to: change.mapPos(to) }) + }); + this.removeUnderlineEffect = StateEffect.define(); + + this.underlineDecoration = Decoration.mark({ class: 'cm-underline' }); + + const that = this; + const underlineExtension = StateField.define({ + create() { + return Decoration.none; + }, + update(value, transaction) { + + // Move the decorations to account for document changes + value = value.map(transaction.changes); + + for (const effect of transaction.effects) { + if (effect.is(that.addUnderlineEffect)) { + value = value.update({ + add: [that.underlineDecoration.range(effect.value.from, effect.value.to)], + }); + } else if (effect.is(that.removeUnderlineEffect)) { + value = value.update({ + filter: (f, t, value) => false, + }); + } + } + return value; + }, + provide: f => EditorView.decorations.from(f) + }); + + const state = EditorState.create({ + doc: this.initialDocString, + extensions: [ + provideCodeMirrorSetup({ + oneline: true, + placeholder: this.placeholder(), + paddingLeft: this.icon() ? '24px' : undefined, + onEnter: view => { + this.onEnter.emit(view.state.doc.toString()); + }, + completions: this.completions(), + }), + filter(), + EditorView.updateListener.of(update => { + if (update.docChanged) { + const newValue = update.state.doc.toString(); + this.onChange(newValue); + this.showClear.set(!!newValue); + } + }), + underlineExtension, + ], + }); + + this.editorView = new EditorView({ + state, + parent: targetEl, + }); + this.showClear.set(!!this.initialDocString); + } + + ngOnDestroy(): void { + this.editorView?.destroy(); + this.editorView = null; + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.css new file mode 100644 index 00000000000..533941a9491 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.css @@ -0,0 +1,3 @@ +.editor-container { + border: 1px solid rgba(0, 0, 0, 0.1); +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.html new file mode 100644 index 00000000000..f3d092e93a7 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.html @@ -0,0 +1 @@ +
diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.ts new file mode 100644 index 00000000000..472267211a0 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/filter-textarea.component.ts @@ -0,0 +1,148 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, effect, ElementRef, forwardRef, input, OnDestroy, output, viewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Completion } from '@codemirror/autocomplete'; +import { EditorState, StateEffect, StateEffectType, StateField } from '@codemirror/state'; +import { Decoration, DecorationSet } from "@codemirror/view"; +import { EditorView } from 'codemirror'; +import { provideCodeMirrorSetup } from './cmSetup'; +import { FilterErrorMark } from './FilterErrorMark'; +import { filter } from './lang-filter'; + +@Component({ + standalone: true, + selector: 'ya-filter-textarea', + templateUrl: './filter-textarea.component.html', + styleUrl: './filter-textarea.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => YaFilterTextarea), + multi: true, + }], +}) +export class YaFilterTextarea implements ControlValueAccessor, AfterViewInit, OnDestroy { + + errorMark = input(); + placeholder = input(); + completions = input(); + + onEnter = output(); + + private editorContainerRef = viewChild.required>('editorContainer'); + + private editorView: EditorView | null = null; + private underlineDecoration: Decoration; + private addUnderlineEffect: StateEffectType<{ from: number; to: number; }>; + private removeUnderlineEffect: StateEffectType; + + private onChange = (_: string | null) => { }; + + // Internal value, for when a value is received before CM init + private initialDocString: string | undefined; + + constructor() { + effect(() => { + const errorMark = this.errorMark(); + + // Remove any old effect, before adding a new one + this.editorView!.dispatch({ + effects: this.removeUnderlineEffect.of(null), + }); + + if (errorMark) { + const { doc } = this.editorView!.state; + const { beginLine, beginColumn, endLine, endColumn } = errorMark; + + const beginOffset = doc.line(beginLine).from + (beginColumn - 1); + const endOffset = doc.line(endLine).from + endColumn; + this.editorView!.dispatch({ + effects: this.addUnderlineEffect.of(this.underlineDecoration.range(beginOffset, endOffset)), + }); + } + }); + } + + writeValue(value: any): void { + this.initialDocString = value || undefined; + this.editorView?.dispatch({ + changes: { + from: 0, + to: this.editorView.state.doc.length, + insert: this.initialDocString, + }, + }); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngAfterViewInit(): void { + const targetEl = this.editorContainerRef().nativeElement; + this.initializeEditor(targetEl); + } + + private initializeEditor(targetEl: HTMLDivElement) { + this.addUnderlineEffect = StateEffect.define({ + map: ({ from, to }, change) => ({ from: change.mapPos(from), to: change.mapPos(to) }) + }); + this.removeUnderlineEffect = StateEffect.define(); + + this.underlineDecoration = Decoration.mark({ class: 'cm-underline' }); + + const that = this; + const underlineExtension = StateField.define({ + create() { + return Decoration.none; + }, + update(value, transaction) { + + // Move the decorations to account for document changes + value = value.map(transaction.changes); + + for (const effect of transaction.effects) { + if (effect.is(that.addUnderlineEffect)) { + value = value.update({ + add: [that.underlineDecoration.range(effect.value.from, effect.value.to)], + }); + } else if (effect.is(that.removeUnderlineEffect)) { + value = value.update({ + filter: (f, t, value) => false, + }); + } + } + return value; + }, + provide: f => EditorView.decorations.from(f) + }); + + const state = EditorState.create({ + doc: this.initialDocString, + extensions: [ + provideCodeMirrorSetup({ + completions: this.completions(), + }), + filter(), + EditorView.updateListener.of(update => { + if (update.docChanged) { + this.onChange(update.state.doc.toString()); + } + }), + underlineExtension, + ], + }); + + this.editorView = new EditorView({ + state, + parent: targetEl, + }); + } + + ngOnDestroy(): void { + this.editorView?.destroy(); + this.editorView = null; + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/highlight.js b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/highlight.js new file mode 100644 index 00000000000..41c85160f62 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/highlight.js @@ -0,0 +1,15 @@ +import { styleTags, tags as t } from "@lezer/highlight"; + +export const filterHighlighting = styleTags({ + String: t.string, + Text: t.literal, + LineComment: t.lineComment, + CompareOp: t.compareOperator, + Comparable: t.propertyName, + LogicOp: t.logicOperator, + Number: t.number, + "True False": t.bool, + Minus: t.operator, + Null: t.null, + "( )": t.paren +}) diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/lang-filter.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/lang-filter.ts new file mode 100644 index 00000000000..183fe78d433 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/lang-filter.ts @@ -0,0 +1,14 @@ +import { LanguageSupport, LRLanguage } from "@codemirror/language"; +import { parser } from "./parser"; + +export const filterLanguage = LRLanguage.define({ + name: "filter", + parser: parser.configure({}), + languageData: { + closeBrackets: { brackets: ["(", '"'] }, + } +}); + +export function filter() { + return new LanguageSupport(filterLanguage); +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.js b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.js new file mode 100644 index 00000000000..64e4844a68f --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.js @@ -0,0 +1,21 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +import {filterHighlighting} from "./highlight" +export const parser = LRParser.deserialize({ + version: 14, + states: "#SQ`QPOOO!oQPO'#CfO#fQPO'#CgOOQO'#Cp'#CpO$]QPO'#CpO$eQPO'#ChOOQO'#Ck'#CkOOQO'#Cl'#ClQ`QPOOOOQO'#Cf'#CfOOQO'#Cg'#CgOOQO,59[,59[OOQO'#Cj'#CjO%SQPO,59SOOQO-E6j-E6jOOQO1G.n1G.n", + stateData: "%v~OcOSPOSQOSROS~OTROUROVROWROXSOePOfQOpUOqUOrUO~Og]Xh]Xi]Xj]Xk]Xl]Xm]Xn]Xo]X~OTYXUYXVYXWYXXYXaYXeYXfYXpYXqYXrYX~P!QOTZXUZXVZXWZXXZXaZXeZXfZXpZXqZXrZX~P!QOeXOfYO~Og[Oh[Oi[Oj[Ok[Ol[Om[On[Oo[O~OTROUROVROWROXSOeXOfYO~OTrqpWUPXVf~", + goto: "!WePPPPPPPPPPffnrvnyPPP!PUROW]RZSTVOWTTOWR]TQWOR^WSVOWR_]", + nodeNames: "âš  LineComment ( ) Filter True False Null Number Minus String Text Comparison Comparable CompareOp LogicOp", + maxTerm: 34, + nodeProps: [ + ["isolate", -3,10,11,13,""] + ], + propSources: [filterHighlighting], + skippedNodes: [0,1,2,3], + repeatNodeCount: 1, + tokenData: "5p~RmXY!|YZ!|]^!|pq!|qr#Rrs#ftu$yxy%byz%g}!O%l!Q!R&c!R!['q![!](S!^!_(X!_!`(f!`!a(s!c!d)Q!d!p$y!p!q*w!q!r,n!r!}$y#R#S$y#T#Y$y#Y#Z-v#Z#b$y#b#c0v#c#h$y#h#i3[#i#o$y~#ROc~~#UQ!_!`#[#r#s#a~#aOh~~#fOn~~#iWpq#fqr#frs$Rs#O#f#O#P$W#P;'S#f;'S;=`$s<%lO#f~$WOe~~$ZWrs#f!P!Q#f#O#P#f#U#V#f#Y#Z#f#b#c#f#f#g#f#h#i#f~$vP;=`<%l#f~%OUf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~%gOQ~~%lOR~~%qRX~}!O%z!Q!R&c!R!['q~&PSP~OY%zZ;'S%z;'S;=`&]<%lO%z~&`P;=`<%l%z~&hRW~!O!P&q!g!h'V#X#Y'V~&tP!Q![&w~&|RW~!Q![&w!g!h'V#X#Y'V~'YR{|'c}!O'c!Q!['i~'fP!Q!['i~'nPW~!Q!['i~'vSW~!O!P&q!Q!['q!g!h'V#X#Y'V~(XOo~~(^Pi~!_!`(a~(fOk~~(kPg~#r#s(n~(sOm~~(xPj~!_!`({~)QOl~~)VWf~tu$y!O!P$y!Q![$y!c!p$y!p!q)o!q!}$y#R#S$y#T#o$y~)tWf~tu$y!O!P$y!Q![$y!c!f$y!f!g*^!g!}$y#R#S$y#T#o$y~*eUp~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~*|Wf~tu$y!O!P$y!Q![$y!c!q$y!q!r+f!r!}$y#R#S$y#T#o$y~+kWf~tu$y!O!P$y!Q![$y!c!v$y!v!w,T!w!}$y#R#S$y#T#o$y~,[Ur~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~,sWf~tu$y!O!P$y!Q![$y!c!t$y!t!u-]!u!}$y#R#S$y#T#o$y~-dUq~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~-{Vf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#U.b#U#o$y~.gWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#`$y#`#a/P#a#o$y~/UWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#g$y#g#h/n#h#o$y~/sWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#X$y#X#Y0]#Y#o$y~0dUU~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~0{Wf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#i$y#i#j1e#j#o$y~1jWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#`$y#`#a2S#a#o$y~2XWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#`$y#`#a2q#a#o$y~2xUV~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y~3aWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#f$y#f#g3y#g#o$y~4OWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#i$y#i#j4h#j#o$y~4mWf~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#X$y#X#Y5V#Y#o$y~5^UT~f~tu$y!O!P$y!Q![$y!c!}$y#R#S$y#T#o$y", + tokenizers: [0], + topRules: {"Filter":[0,4]}, + tokenPrec: 211 +}) diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.terms.js b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.terms.js new file mode 100644 index 00000000000..ec7c6c8c6c6 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/filter/parser.terms.js @@ -0,0 +1,15 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + LineComment = 1, + Filter = 4, + True = 5, + False = 6, + Null = 7, + Number = 8, + Minus = 9, + String = 10, + Text = 11, + Comparison = 12, + Comparable = 13, + CompareOp = 14, + LogicOp = 15 diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/message-bar/message-bar.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/message-bar/message-bar.component.css index 8ae1c1dc39f..9f6f047df3b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/message-bar/message-bar.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/message-bar/message-bar.component.css @@ -28,6 +28,12 @@ line-height: 48px; } +.actions button { + margin-top: 4px; + color: #fff; + margin-right: 11px; +} + .message-bar.warning .actions { background-color: darkorange !important; } diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css new file mode 100644 index 00000000000..9f5bb360778 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css @@ -0,0 +1,26 @@ +.search-input { + position: relative; + display: flex; +} + +.search-input input { + flex: 1 1 auto; + padding-left: 24px; + padding-right: 24px; +} + +.ya-button.group { + border-left: 0; + margin-right: 0; +} + +.query-container-and-error { + width: 100%; + display: flex; + flex-direction: column; + position: relative; +} + +.query-error { + color: crimson; +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html new file mode 100644 index 00000000000..2db4526a565 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html @@ -0,0 +1,29 @@ +
+
+ @if (expanded()) { + + } @else { + + } + @if (errorState$ | async; as errorState) { +
+ {{ errorState.message }} +
+ } +
+ + +
diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts new file mode 100644 index 00000000000..5374554d479 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts @@ -0,0 +1,144 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, forwardRef, input, OnDestroy, output, signal, viewChild } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { Completion } from '@codemirror/autocomplete'; +import { BehaviorSubject, debounceTime, Subscription } from 'rxjs'; +import { YaFilterInput } from '../filter/filter-input.component'; +import { YaFilterTextarea } from '../filter/filter-textarea.component'; +import { FilterErrorMark } from '../filter/FilterErrorMark'; + +interface ErrorState { + message: string; + context?: FilterErrorMark; +} + +@Component({ + standalone: true, + selector: 'ya-search-filter2', + templateUrl: './search-filter2.component.html', + styleUrl: './search-filter2.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => YaSearchFilter2), + multi: true, + }], + imports: [ + AsyncPipe, + MatIcon, + MatTooltip, + ReactiveFormsModule, + YaFilterInput, + YaFilterTextarea, + ], +}) +export class YaSearchFilter2 implements ControlValueAccessor, OnDestroy { + + placeholder = input('Filter'); + width = input('100%'); + debounceTime = input(400); + expanded = input(false); + completions = input(); + + /** + * True if an unsubmitted filter is pending + */ + dirty = signal(false); + + /** + * Latest typed value (including, before the user has pressed search). + * + * By default, debounced at 400ms. + */ + typedValue = output(); + + /** + * True if the current filter field is empty. This uses the + * typed value, rather than the submitted value. + */ + empty = signal(false); + + /** + * Actual current value (when search is pressed). + * + * Shouldn't need to be stored, but we do it so that we can + * detect dirty flag. + */ + private value: string; + + public onelineFilterInput = viewChild('oneline'); + + // Keep this a subject, not a signal. + // When it's a signal, mysterious things happen with CM duplicating. + errorState$ = new BehaviorSubject(null); + + private onChange = (_: string | null) => { }; + + // Form model, either managed with an HTML Input, or a CodeMirror editor. + // This control updates instantly, as opposed to the exposed value. + formControl = new FormControl(null); + + private subscriptions: Subscription[] = []; + + constructor() { + const formSubscription = this.formControl.valueChanges.subscribe(value => { + this.empty.set(!!value); + this.dirty.set((this.value || '') !== (value || '')); + }); + this.subscriptions.push(formSubscription); + + const delayedFormSubscription = this.formControl.valueChanges.pipe( + debounceTime(this.debounceTime()), + ).subscribe(value => { + this.typedValue.emit(value || ''); + }); + this.subscriptions.push(delayedFormSubscription); + } + + /** + * Returns the currently visible value (could be unsubmitted). + */ + getTypedValue() { + return this.formControl.value; + } + + addErrorMark(message: string, context: FilterErrorMark) { + this.errorState$.next({ message, context }); + } + + clearErrorMark() { + this.errorState$.next(null); + } + + getValue() { + return this.formControl.value; + } + + writeValue(value: any) { + this.value = value ?? null; + this.formControl.setValue(this.value); + } + + registerOnChange(fn: any) { + this.onChange = fn; + } + + registerOnTouched(fn: any) { + } + + doSearch() { + if (this.errorState$.value) { + return; + } + + this.value = this.formControl.value ?? ''; + this.onChange(this.value); + this.dirty.set(false); + } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.css new file mode 100644 index 00000000000..4c7a3779154 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.css @@ -0,0 +1,21 @@ +:host { + background-color: #e6f1ef; + color: var(--y-accent); + border: 1px solid var(--y-border-color); + border-bottom: 0; + padding-left: 8px; + padding-right: 8px; + height: 26px; + + display: flex; + align-items: center; +} + +:host.error { + background-color: #fcc; + color: crimson; +} + +:host>mat-icon { + margin-right: 4px; +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.html new file mode 100644 index 00000000000..d0677689900 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.html @@ -0,0 +1,12 @@ + + @if (icon() === "auto") { + @if (severity() === "info") { + info + } @else if (severity() === "error") { + error_outline + } + } @else { + {{ icon() }} + } + + diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.ts new file mode 100644 index 00000000000..8d835e5dc21 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/table-top/table-top.component.ts @@ -0,0 +1,22 @@ +import { Component, HostBinding, input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + standalone: true, + selector: 'ya-table-top', + templateUrl: './table-top.component.html', + styleUrl: './table-top.component.css', + imports: [ + MatIcon, + ], +}) +export class YaTableTop { + + icon = input('auto'); + severity = input<'info' | 'error'>('info'); + + @HostBinding('class.error') + get error() { + return this.severity() === 'error'; + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts index 752f1b08a3a..c22d2bdc146 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts @@ -38,6 +38,8 @@ import { YaDurationInput } from './components/duration-input/duration-input.comp import { YaEmptyMessage } from './components/empty-message/empty-message.component'; import { YaErrors } from './components/errors/errors.component'; import { YaExpirable } from './components/expirable/expirable.component'; +import { YaFilterInput } from './components/filter/filter-input.component'; +import { YaFilterTextarea } from './components/filter/filter-textarea.component'; import { YaHelp } from './components/help/help.component'; import { YaHelpDialog } from './components/help/help.dialog'; import { YaHexIntegerInput } from './components/hex-integer-input/hex-integer-input.component'; @@ -53,10 +55,12 @@ import { YaMultiSelect } from './components/multi-select/multi-select.component' import { YaOption } from './components/option/option.component'; import { YaProgress } from './components/progress/progress.component'; import { YaSearchFilter } from './components/search-filter/search-filter.component'; +import { YaSearchFilter2 } from './components/search-filter2/search-filter2.component'; import { YaSelect } from './components/select/select.component'; import { YaSidebarNavGroup } from './components/sidebar/sidebar-nav-group.component'; import { YaSidebarNavItem } from './components/sidebar/sidebar-nav-item.component'; import { YaTableToggle } from './components/table-toggle/table-toggle.component'; +import { YaTableTop } from './components/table-top/table-top.component'; import { YaTagSelect } from './components/tag-select/tag-select.component'; import { YaTextAction } from './components/text-action/text-action.component'; import { YaTimezoneSelect } from './components/timezone-select/timezone-select.component'; @@ -158,6 +162,8 @@ const sharedComponents = [ YaEmptyMessage, YaErrors, YaExpirable, + YaFilterInput, + YaFilterTextarea, YaHelp, YaHelpDialog, YaHexIntegerInput, @@ -174,10 +180,12 @@ const sharedComponents = [ YaProgress, YaPrintZone, YaSearchFilter, + YaSearchFilter2, YaSelect, YaSidebarNavGroup, YaSidebarNavItem, YaTableToggle, + YaTableTop, YaTagSelect, YaTextAction, YaTimezoneSelect, diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts index 0ae571bf54b..de1b4b39e52 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts @@ -20,6 +20,8 @@ export * from './lib/components/duration-input/duration-input.component'; export * from './lib/components/empty-message/empty-message.component'; export * from './lib/components/errors/errors.component'; export * from './lib/components/expirable/expirable.component'; +export * from './lib/components/filter/filter-input.component'; +export * from './lib/components/filter/filter-textarea.component'; export * from './lib/components/help/help.component'; export * from './lib/components/help/help.dialog'; export * from './lib/components/hex-integer-input/hex-integer-input.component'; @@ -35,10 +37,12 @@ export * from './lib/components/multi-select/multi-select.component'; export * from './lib/components/option/option.component'; export * from './lib/components/progress/progress.component'; export * from './lib/components/search-filter/search-filter.component'; +export * from './lib/components/search-filter2/search-filter2.component'; export * from './lib/components/select/select.component'; export * from './lib/components/sidebar/sidebar-nav-group.component'; export * from './lib/components/sidebar/sidebar-nav-item.component'; export * from './lib/components/table-toggle/table-toggle.component'; +export * from './lib/components/table-top/table-top.component'; export * from './lib/components/tag-select/tag-select.component'; export * from './lib/components/text-action/text-action.component'; export * from './lib/components/timezone-select/timezone-select.component'; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css index 7d18476dfc1..89e2c99d4c2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css @@ -126,7 +126,6 @@ a.ya-link { .ya-data-table a:hover, .ya-link a:hover, a.ya-link:hover { - /*color: #1b61b9;*/ text-decoration: underline; } @@ -179,6 +178,15 @@ a.ya-link:hover { font-family: Roboto, sans-serif; } +.table-actions { + font-family: Roboto, sans-serif; + display: flex; +} + +.table-actions .ya-button { + margin-left: 7px; +} + @media print { .ya-data-table { width: auto !important; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css index 196b93b13b4..6e0a14be25d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css @@ -33,7 +33,9 @@ .ya-form div.label input[type="password"], .ya-form div.label textarea, .ya-form div.label select, -.ya-form div.label .date-time-input { +.ya-form div.label .date-time-input, +.ya-form div.label ya-filter-input, +.ya-form div.label ya-filter-textarea { margin: 5px 0 0; display: block; width: 100%; @@ -54,6 +56,7 @@ background: white; border: 1px solid #d3d3d3; border-radius: 1px; + font-family: Roboto, sans-serif; font-size: 12px; font-weight: 400; height: 24px; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/material-theme.scss b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/material-theme.scss index 09c3e6b4866..c55a5a90187 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/material-theme.scss +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/material-theme.scss @@ -56,6 +56,7 @@ $ya-theme: mat.define-theme(( --mdc-dialog-container-color: #fff; --mdc-dialog-container-shape: 10px; --mat-dialog-container-elevation-shadow: 0px 11px 15px -7px rgba(0, 0, 0, .2), 0px 24px 38px 3px rgba(0, 0, 0, .14), 0px 9px 46px 8px rgba(0, 0, 0, .12); + --mat-dialog-container-max-width: 60vw; --mat-dialog-actions-padding: 16px 24px; --mat-dialog-content-padding: 20px 24px; --mat-dialog-with-actions-content-padding: 20px 24px 0; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css index 514dc86a0ca..b27fce0db9b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css @@ -265,6 +265,7 @@ button.ya-button { border-radius: 1px; background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.02)); cursor: pointer; + font: 400 12px / 12px Roboto, sans-serif; } button.ya-button:active { @@ -297,6 +298,17 @@ button.ya-button.primary { border-color: rgba(0, 0, 0, 0.1); } +button.ya-button.text-only { + border: 0; + background: inherit; + color: var(--y-accent); +} + +button.ya-button.text-only:hover { + box-shadow: none; + background-color: #e6f1ef; +} + .mat-mdc-tab-nav-bar.secondary .mat-mdc-tab-link, .mat-mdc-tab-group.secondary .mat-mdc-tab { height: 36px; diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html index d275137bd2a..d9940bfe032 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html @@ -98,7 +98,7 @@ } - + @@ -125,7 +125,7 @@ @if (dataSource.streaming$ | async) {
Listening for commands - +
} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.ts index 9819b3abba3..3619800ffcb 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.ts @@ -5,6 +5,7 @@ import { WebappSdkModule, YaSelectOption, YamcsService, utils } from '@yamcs/web @Component({ standalone: true, + selector: 'app-create-event-dialog', templateUrl: './create-event-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html new file mode 100644 index 00000000000..074887ac3ac --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html @@ -0,0 +1,48 @@ +

Save query

+ + +
+
+ Name + +
+
+
+ Query + +
+
+ +
+ +
+
+ Share with other users +
+ +
+
+
+ + + + + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.ts new file mode 100644 index 00000000000..786bb764440 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { EventSeverity, MessageService, WebappSdkModule, YamcsService } from '@yamcs/webapp-sdk'; + +const defaultSeverity: EventSeverity = 'INFO'; +const defaultSource: string[] = []; + +@Component({ + standalone: true, + selector: 'app-create-event-query-dialog', + templateUrl: './create-event-query-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + WebappSdkModule, + ], +}) +export class CreateEventQueryDialogComponent { + + form = new FormGroup({ + name: new FormControl('', Validators.required), + filter: new FormControl(''), + severity: new FormControl(defaultSeverity, Validators.required), + source: new FormControl(defaultSource), + shared: new FormControl(false, Validators.required), + }); + + constructor( + private dialogRef: MatDialogRef, + private yamcs: YamcsService, + private messageService: MessageService, + @Inject(MAT_DIALOG_DATA) readonly data: any, + ) { + this.form.patchValue({ + filter: data.filter || undefined, + severity: data.severity, + source: data.source, + }); + } + + save() { + const { value: fv } = this.form; + this.yamcs.yamcsClient.createQuery(this.yamcs.instance!, 'events', { + name: fv.name!, + shared: fv.shared ?? false, + query: { + filter: fv.filter ?? undefined, + source: fv.source ?? defaultSource, + severity: fv.severity ?? defaultSeverity, + }, + }).then(query => this.dialogRef.close(query)) + .catch(err => this.messageService.showError(err)); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html new file mode 100644 index 00000000000..39dc57db9a0 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html @@ -0,0 +1,48 @@ +

Edit query

+ + +
+
+ Name + +
+
+
+ Query + +
+
+ +
+ +
+
+ Share with other users +
+ +
+
+
+ + + + + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.ts new file mode 100644 index 00000000000..fd27fc31c37 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.ts @@ -0,0 +1,58 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { EventSeverity, MessageService, WebappSdkModule, YamcsService } from '@yamcs/webapp-sdk'; + +const defaultSeverity: EventSeverity = 'INFO'; +const defaultSource: string[] = []; + +@Component({ + standalone: true, + selector: 'app-edit-event-query-dialog', + templateUrl: './edit-event-query-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + WebappSdkModule, + ], +}) +export class EditEventQueryDialogComponent { + + form = new FormGroup({ + name: new FormControl('', Validators.required), + filter: new FormControl(''), + severity: new FormControl(defaultSeverity, Validators.required), + source: new FormControl(defaultSource), + shared: new FormControl(false, Validators.required), + }); + + constructor( + private dialogRef: MatDialogRef, + private yamcs: YamcsService, + private messageService: MessageService, + @Inject(MAT_DIALOG_DATA) readonly data: any, + ) { + const { query } = data; + this.form.patchValue({ + name: query.name, + filter: query.query.filter || undefined, + severity: query.query.severity, + source: query.query.source, + shared: query.shared, + }); + } + + save() { + const { value: fv } = this.form; + const queryId: string = this.data.query.id; + this.yamcs.yamcsClient.editQuery(this.yamcs.instance!, 'events', queryId, { + name: fv.name!, + shared: fv.shared ?? false, + query: { + filter: fv.filter ?? undefined, + source: fv.source ?? defaultSource, + severity: fv.severity ?? defaultSeverity, + }, + }).then(query => this.dialogRef.close(query)) + .catch(err => this.messageService.showError(err)); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/completions.ts b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/completions.ts new file mode 100644 index 00000000000..2f503d49de0 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/completions.ts @@ -0,0 +1,108 @@ +import { Completion, insertCompletionText } from '@codemirror/autocomplete'; +import { EditorView } from 'codemirror'; + +function applyString(view: EditorView, completion: Completion, from: number, to: number) { + const replacement = completion.label + ' = \"\"'; + const tr = insertCompletionText(view.state, replacement, from, to); + // Place cursor between quotes + tr.selection = { anchor: from + replacement.length - 1 }; + view.dispatch(tr); +} + +function applyEnum(view: EditorView, completion: Completion, from: number, to: number) { + view.dispatch(insertCompletionText(view.state, completion.label + ' = ', from, to)); +} + +function applyNumber(view: EditorView, completion: Completion, from: number, to: number) { + view.dispatch(insertCompletionText(view.state, completion.label + ' = ', from, to)); +} + +function applyLogicalOperator(view: EditorView, completion: Completion, from: number, to: number) { + view.dispatch(insertCompletionText(view.state, completion.label + ' ', from, to)); +} + +export const EVENT_COMPLETIONS: Completion[] = [ + { + label: 'message', + type: 'method', + info: 'Filter on event message', + apply: applyString, + }, + { + label: 'seqNumber', + type: 'method', + info: 'Filter on event sequence number', + apply: applyNumber, + }, + { + label: 'severity', + type: 'method', + info: 'Filter on event severity (info, watch, warning, distress, critical, severe)', + apply: applyEnum, + }, + { + label: 'source', + type: 'method', + info: 'Filter on event source', + apply: applyString, + }, + { + label: 'type', + type: 'method', + info: 'Filter on event type', + apply: applyString, + }, + { + section: 'Exclude events', + label: '-message', + type: 'method', + info: 'Exclude events based on message', + apply: applyString, + }, + { + section: 'Exclude events', + label: '-seqNumber', + type: 'method', + info: 'Exclude events based on sequence number', + apply: applyNumber, + }, + { + section: 'Exclude events', + label: '-severity', + type: 'method', + info: 'Exclude events based on severity (info, watch, warning, distress, critical, severe)', + apply: applyEnum, + }, + { + section: 'Exclude events', + label: '-source', + type: 'method', + info: 'Exclude events based on source', + apply: applyString, + }, + { + section: 'Exclude events', + label: '-type', + type: 'method', + info: 'Exclude events based on type', + apply: applyString, + }, + { + section: 'Logical operators', + label: 'AND', + type: 'constant', + apply: applyLogicalOperator, + }, + { + section: 'Logical operators', + label: 'OR', + type: 'constant', + apply: applyLogicalOperator, + }, + { + section: 'Logical operators', + label: 'NOT', + type: 'constant', + apply: applyLogicalOperator, + }, +]; diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.css b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.css index 4f65c666507..c47ead4551c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.css @@ -1,3 +1,7 @@ +.filter-bar.query { + height: unset; +} + .table-status { background-color: var(--y-background-color); height: 24px; diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html index 5b28e0bfac6..077605cf41f 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html @@ -31,29 +31,21 @@ @if (dataSource) {
+ + + +
-
- - -
- -
-
- - - - - - - - - +
@@ -68,66 +60,119 @@ Apply } + + + + + + + + + + @if (filterForm.value["interval"] !== "CUSTOM") { } - @if (dataSource.loading$ | async) { - - } - @if (dataSource.streaming$ | async) { -
- Listening for events - -
- } + + Multiline +
-
- @switch (appliedInterval) { - @case ("PT1H") { - - Showing events from - the last hour - ending at - {{ validStop | datetime }} - (Mission Time) - - } - @case ("PT6H") { - - Showing events from - the last 6 hours - ending at - {{ validStop | datetime }} - (Mission Time) - - } - @case ("P1D") { - - Showing events from - the last 24 hours - ending at - {{ validStop | datetime }} - (Mission Time) - - } - @case ("NO_LIMIT") { - - Showing events from - all time - - } - @case ("CUSTOM") { - - Showing events from - {{ validStart | datetime }} - to - {{ validStop | datetime }} - (Mission Time) - +
+
+ @switch (appliedInterval) { + @case ("PT1H") { + + Showing events from + the last hour + ending at + {{ validStop | datetime }} + (Mission Time) + + } + @case ("PT6H") { + + Showing events from + the last 6 hours + ending at + {{ validStop | datetime }} + (Mission Time) + + } + @case ("P1D") { + + Showing events from + the last 24 hours + ending at + {{ validStop | datetime }} + (Mission Time) + + } + @case ("NO_LIMIT") { + + Showing events from + all time + + } + @case ("CUSTOM") { + + Showing events from + {{ validStart | datetime }} + to + {{ validStop | datetime }} + (Mission Time) + + } } +
+
+ @if (dataSource.loading$ | async) { + + } + @if (dataSource.streaming$ | async) { +
+ Listening for events + +
} + + +
+ + @if (searchFilter.dirty()) { + + The search filter has changed. + @if (!(searchFilter.errorState$ | async)) { +   + + Apply filter + + . + } + + } @@ -154,26 +199,28 @@ - + - + - + @for (extraColumn of extraColumns; track extraColumn) { - +
Severity Generation TimeGeneration time {{ (row.generationTime | datetime) || "-" }} Reception TimeReception time {{ (row.receptionTime | datetime) || "-" }} Sequence NumberSequence number {{ row.seqNumber ?? "-" }} {{ extraColumn.label }} + {{ extraColumn.label }} + @if (row.extra) { {{ row.extra[extraColumn.id] ?? "-" }} @@ -189,6 +236,7 @@ *cdkRowDef="let row; columns: columnChooser.displayedColumns$ | async" [ngClass]="row.severity">
+ diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/button/button.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/button/button.component.ts new file mode 100644 index 00000000000..0b12a52e472 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/button/button.component.ts @@ -0,0 +1,30 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; + +export type YaButtonAppearance = 'basic' | 'text' | 'primary'; + +@Component({ + standalone: true, + selector: 'ya-button', + templateUrl: './button.component.html', + styleUrl: './button.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatIcon, + ], +}) +export class YaButton { + + icon = input(); + appearance = input('basic'); + disabled = input(false, { transform: booleanAttribute }); + dropdown = input(false, { transform: booleanAttribute }); + toggled = input(false, { transform: booleanAttribute }); + + click = output(); + + onClick(event: MouseEvent) { + this.click.emit(event); + event.stopPropagation(); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html index 00ea3ebf0b8..2b2609c441e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.html @@ -1,10 +1,10 @@ - + diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts index ad7671ad14a..20b449633ec 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/column-chooser/column-chooser.component.ts @@ -1,9 +1,10 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, Input, OnInit } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { MatMenu, MatMenuContent, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { BehaviorSubject } from 'rxjs'; import { PreferenceStore } from '../../services/preference-store.service'; +import { YaButton } from '../button/button.component'; export interface YaColumnInfo { id: string; @@ -23,7 +24,8 @@ export interface YaColumnInfo { MatMenuContent, MatMenuItem, MatIcon, - MatMenuTrigger + MatMenuTrigger, + YaButton, ], }) export class YaColumnChooser implements OnInit { @@ -31,14 +33,9 @@ export class YaColumnChooser implements OnInit { @Input() columns: YaColumnInfo[]; - @Input() - preferenceKey: string; - - @Input() - icon?: string; - - @Input() - text = false; + preferenceKey = input(); + icon = input(); + appearance = input('basic'); displayedColumns$ = new BehaviorSubject([]); @@ -52,9 +49,10 @@ export class YaColumnChooser implements OnInit { recalculate(columns: YaColumnInfo[]) { this.columns = columns; + const preferenceKey = this.preferenceKey(); let preferredColumns: string[] = []; - if (this.preferenceKey) { - const storedDisplayedColumns = this.preferenceStore.getVisibleColumns(this.preferenceKey); + if (preferenceKey) { + const storedDisplayedColumns = this.preferenceStore.getVisibleColumns(preferenceKey); preferredColumns = (storedDisplayedColumns || []).filter(el => { // Filter out unknown columns for (const column of this.columns) { @@ -82,8 +80,9 @@ export class YaColumnChooser implements OnInit { } writeValue(value: any) { - if (this.preferenceKey) { - this.preferenceStore.setVisibleColumns(this.preferenceKey, value); + const preferenceKey = this.preferenceKey(); + if (preferenceKey) { + this.preferenceStore.setVisibleColumns(preferenceKey, value); } this.displayedColumns$.next(value); } diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.html index 09459f6ffaf..fdb8c3ff41a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.html @@ -1,8 +1,4 @@ - - + + diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.ts index 1939d2562d6..64aac7c3aa2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/download-button/download-button.component.ts @@ -1,20 +1,19 @@ -import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { booleanAttribute, Component, ElementRef, input, ViewChild } from '@angular/core'; +import { YaButton, YaButtonAppearance } from '../button/button.component'; @Component({ standalone: true, selector: 'ya-download-button', templateUrl: './download-button.component.html', + imports: [ + YaButton, + ], }) export class YaDownloadButton { - @Input() - link: string; - - @Input() - disabled = false; - - @Input() - primary = false; + link = input.required(); + disabled = input(false, { transform: booleanAttribute }); + appearance = input('basic'); @ViewChild('hiddenLink', { static: true }) private hiddenLink: ElementRef; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/duration-input/duration-input.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/duration-input/duration-input.component.css index 2d0f13073c3..928fc623c93 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/duration-input/duration-input.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/duration-input/duration-input.component.css @@ -12,7 +12,3 @@ input { width: 100px !important; margin-right: 5px !important; } - -::ng-deep .duration-input .ya-button { - margin: 0 !important; -} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.html index 8332b4c7b57..edb419de6f5 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.html @@ -1,6 +1,6 @@

{{ data.icon || "help" }} - {{ data.title }} + {{ data.title }}

@@ -8,5 +8,5 @@

- + {{ data.closeText || "Close" }} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.ts index 95b2490d21c..964ff6880a0 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/help/help.dialog.ts @@ -1,6 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; import { MatIcon } from '@angular/material/icon'; +import { YaButton } from '../button/button.component'; @Component({ standalone: true, @@ -13,6 +14,7 @@ import { MatIcon } from '@angular/material/icon'; MatDialogContent, MatDialogTitle, MatIcon, + YaButton, ], }) export class YaHelpDialog { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.html index 354265abc37..f44ccbf5ac1 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.html @@ -1,15 +1,11 @@ - + @for (option of options$ | async; track option) { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.ts index 55ba062f0d7..e4c95ee157e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/multi-select/multi-select.component.ts @@ -1,10 +1,11 @@ import { AsyncPipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, OnChanges, forwardRef } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, forwardRef, input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatDivider } from '@angular/material/divider'; import { MatIcon } from '@angular/material/icon'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { BehaviorSubject } from 'rxjs'; +import { YaButton } from '../button/button.component'; import { YaSelectOption } from '../select/select.component'; @Component({ @@ -23,19 +24,17 @@ import { YaSelectOption } from '../select/select.component'; MatIcon, MatMenu, MatMenuItem, - MatMenuTrigger -], + MatMenuTrigger, + YaButton, + ], }) export class YaMultiSelect implements OnChanges, ControlValueAccessor { - @Input() - emptyOption: string = '-- select an option --'; - @Input() options: YaSelectOption[] = []; - @Input() - icon: string; + icon = input(); + emptyOption = input('-- select an option --'); options$ = new BehaviorSubject([]); selected$ = new BehaviorSubject([]); diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css index 9f5bb360778..d777c14eb5e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.css @@ -9,11 +9,6 @@ padding-right: 24px; } -.ya-button.group { - border-left: 0; - margin-right: 0; -} - .query-container-and-error { width: 100%; display: flex; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html index 2db4526a565..019e538c1ed 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.html @@ -23,7 +23,7 @@ }

- +
diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts index 5374554d479..4c54e54a8a5 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/search-filter2/search-filter2.component.ts @@ -5,6 +5,7 @@ import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { Completion } from '@codemirror/autocomplete'; import { BehaviorSubject, debounceTime, Subscription } from 'rxjs'; +import { YaButton } from '../button/button.component'; import { YaFilterInput } from '../filter/filter-input.component'; import { YaFilterTextarea } from '../filter/filter-textarea.component'; import { FilterErrorMark } from '../filter/FilterErrorMark'; @@ -30,6 +31,7 @@ interface ErrorState { MatIcon, MatTooltip, ReactiveFormsModule, + YaButton, YaFilterInput, YaFilterTextarea, ], diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.html index 291960d83aa..5c89a44d554 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.html @@ -1,14 +1,10 @@ - + @for (option of options(); track option) { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.ts index 245fef5aae8..1769e7ff91e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/select/select.component.ts @@ -3,6 +3,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatDivider } from '@angular/material/divider'; import { MatIcon } from '@angular/material/icon'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; +import { YaButton } from '../button/button.component'; import { YaOption } from '../option/option.component'; export interface YaSelectOption { @@ -27,7 +28,8 @@ export interface YaSelectOption { MatIcon, MatMenu, MatMenuItem, - MatMenuTrigger + MatMenuTrigger, + YaButton, ], }) export class YaSelect implements ControlValueAccessor { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.html index 5b225196115..1d638bc2581 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.html @@ -16,5 +16,5 @@ (keydown.enter)="addTag(); $event.preventDefault()" placeholder="my-tag" style="width: 120px" /> - + Add
diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.ts index 715afad6e6d..5a11cfc55bc 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/tag-select/tag-select.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { MatIcon } from '@angular/material/icon'; import { BehaviorSubject } from 'rxjs'; +import { YaButton } from '../button/button.component'; import { YaLabel } from '../label/label.component'; @Component({ @@ -20,8 +21,9 @@ import { YaLabel } from '../label/label.component'; AsyncPipe, MatIcon, ReactiveFormsModule, - YaLabel -], + YaButton, + YaLabel, + ], }) export class YaTagSelect implements ControlValueAccessor { diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts index c22d2bdc146..e1ee5c71d8c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts @@ -27,6 +27,7 @@ import { StorageUrlPipe } from '../public-api'; import { YaBinaryInput } from './components/binary-input/binary-input.component'; import { YaBreadcrumbTrail } from './components/breadcrumb/breadcrumb-trail.component'; import { YaBreadcrumb } from './components/breadcrumb/breadcrumb.component'; +import { YaButton } from './components/button/button.component'; import { YaColumnChooser } from './components/column-chooser/column-chooser.component'; import { YaDateTimeInput } from './components/date-time-input/date-time-input.component'; import { YaDetailPane } from './components/detail-pane/detail-pane.component'; @@ -151,6 +152,7 @@ const sharedComponents = [ YaBinaryInput, YaBreadcrumb, YaBreadcrumbTrail, + YaButton, YaColumnChooser, YaDateTimeInput, YaDetailPane, diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts index de1b4b39e52..c64ce2377ea 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts @@ -8,6 +8,7 @@ export * from './lib/commanding/StackFormatter'; export * from './lib/components/binary-input/binary-input.component'; export * from './lib/components/breadcrumb/breadcrumb-trail.component'; export * from './lib/components/breadcrumb/breadcrumb.component'; +export * from './lib/components/button/button.component'; export * from './lib/components/column-chooser/column-chooser.component'; export * from './lib/components/date-time-input/date-time-input.component'; export * from './lib/components/date-time-input/UtcDateAdapter'; diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css index 89e2c99d4c2..2777816c097 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/data-table.css @@ -183,7 +183,7 @@ a.ya-link:hover { display: flex; } -.table-actions .ya-button { +.table-actions ya-button { margin-left: 7px; } diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css index 6e0a14be25d..1e4d6d06b12 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/form.css @@ -41,10 +41,9 @@ width: 100%; } -.ya-form label .ya-button, -.ya-form div.label .ya-button { +.ya-form div.label ya-button { margin: 5px 0 0; - display: block; + display: inline-block; } .ya-form input[type="text"], diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css index b27fce0db9b..70fefe62655 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/styles/styles.css @@ -208,11 +208,6 @@ input.ng-invalid:not(.ng-pristine), font-family: "Roboto"; } -button.expandable { - margin-left: 0.5em; - padding: 0 8px; -} - .hide, .noDisplay { display: none; @@ -254,61 +249,6 @@ button.expandable { background-color: rgba(0, 0, 0, 0.04); } -button.ya-button { - height: 24px; - border: 1px solid #d3d3d3; - background-color: #fff; - outline: none; - color: rgba(0, 0, 0, 0.75); - padding: 1px 6px; - margin: 0; - border-radius: 1px; - background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.02)); - cursor: pointer; - font: 400 12px / 12px Roboto, sans-serif; -} - -button.ya-button:active { - border-color: #bbb; - color: rgba(0, 0, 0, 0.75); - background-image: linear-gradient(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)); -} - -button.ya-button:hover { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); -} - -button.ya-button:disabled { - /*color: rgba(0, 0, 0, .25);*/ - opacity: 0.6; - cursor: default; -} - -button.ya-button .material-icons { - font-size: 14px; - height: 14px; - width: 14px; - /*display: inline-flex;*/ - vertical-align: middle; -} - -button.ya-button.primary { - background-color: var(--y-accent); - color: white; - border-color: rgba(0, 0, 0, 0.1); -} - -button.ya-button.text-only { - border: 0; - background: inherit; - color: var(--y-accent); -} - -button.ya-button.text-only:hover { - box-shadow: none; - background-color: #e6f1ef; -} - .mat-mdc-tab-nav-bar.secondary .mat-mdc-tab-link, .mat-mdc-tab-group.secondary .mat-mdc-tab { height: 36px; @@ -348,14 +288,20 @@ input[type="radio"] { color: whitesmoke !important; } -.action-bar .ya-button, -.mat-mdc-dialog-actions .ya-button:not(:last-child), -.filter-bar .date-time-input, -.filter-bar .ya-button, +.action-bar ya-button, +.mat-mdc-dialog-actions ya-button:not(:last-child), +.filter-bar>.date-time-input, +.filter-bar>ya-button, +.filter-bar>ya-select, +.filter-bar>ya-multi-select, .filter-bar .search-input { margin-right: 7px; } +.filter-bar ya-button.no-margin { + margin-right: 0; +} + .mat-mdc-paginator { background-color: inherit !important; } @@ -435,7 +381,6 @@ p { .mat-drawer, .mat-toolbar, - button.expandable, .filter-bar { display: none !important; } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html index 48595f807a9..a554e93d597 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html @@ -33,12 +33,10 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } @if (dataSource.loading$ | async) { @@ -182,15 +180,9 @@ } @if (row.type === "MANUAL" && row.status === "RUNNING" && mayControlActivities()) { - + Set successful   - + Set failed } @@ -205,9 +197,7 @@ - + Load more diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.css b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.css index a42a483f80a..8b903c51aa1 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.css @@ -15,7 +15,7 @@ display: flex; } -.log-actions .ya-button { +.log-actions ya-button { margin-right: 7px; } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.html index b28e4e871f6..456ac1219a7 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-log-tab/activity-log-tab.component.html @@ -1,12 +1,8 @@
- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/set-failed-dialog/set-failed-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/set-failed-dialog/set-failed-dialog.component.html index 416547b2f40..e9109aba9ad 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/set-failed-dialog/set-failed-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/set-failed-dialog/set-failed-dialog.component.html @@ -10,6 +10,6 @@

Set failed

- - + CANCEL + SUBMIT diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/start-manual-activity-dialog/start-manual-activity-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/start-manual-activity-dialog/start-manual-activity-dialog.component.html index 1d7d6bd8ef5..b3872856e79 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/start-manual-activity-dialog/start-manual-activity-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/start-manual-activity-dialog/start-manual-activity-dialog.component.html @@ -2,18 +2,22 @@

Start manual activity

- Use this dialog to start a manual activity. This is an activity whose execution status - is not managed by Yamcs, but directly by the user. + Use this dialog to start a + manual + activity. This is an activity whose execution status is not managed by Yamcs, but directly by + the user.

- Name (required)
+ Name + (required) +
- - + CANCEL + START diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/admin-action-log/admin-action-log.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/admin-action-log/admin-action-log.component.html index d7c603ef2f6..360cd5fc2e6 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/admin-action-log/admin-action-log.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/admin-action-log/admin-action-log.component.html @@ -9,11 +9,9 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @else { - + Jump to now }
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/show-enum-dialog/show-enum-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/show-enum-dialog/show-enum-dialog.component.html index cc15ea099be..443e5c9c930 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/show-enum-dialog/show-enum-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/show-enum-dialog/show-enum-dialog.component.html @@ -16,5 +16,5 @@

Enum States

- + Close diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-members-dialog/add-members-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-members-dialog/add-members-dialog.component.html index 90328a328af..1e7679b859d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-members-dialog/add-members-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-members-dialog/add-members-dialog.component.html @@ -39,13 +39,13 @@

Add members

} @if (!dataSource.data.length) { - No rows to display + No rows to display } - - + CANCEL + ADD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-roles-dialog/add-roles-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-roles-dialog/add-roles-dialog.component.html index 5fb0f4afe63..a11cadf3dc9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-roles-dialog/add-roles-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/add-roles-dialog/add-roles-dialog.component.html @@ -31,13 +31,13 @@

Add roles

} @if (!dataSource.data.length) { - No rows to display + No rows to display } - - + CANCEL + ADD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/application-credentials-dialog/application-credentials-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/application-credentials-dialog/application-credentials-dialog.component.html index a876fe589ac..808f0c52a9b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/application-credentials-dialog/application-credentials-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/application-credentials-dialog/application-credentials-dialog.component.html @@ -17,10 +17,11 @@

Service account created

- Important: Take note of these application credentials. It is not possible to - display them after closing this dialog. + Important: + Take note of these application credentials. It is not possible to display them after closing this + dialog. - + CLOSE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/change-user-password-dialog/change-user-password-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/change-user-password-dialog/change-user-password-dialog.component.html index 7c699f7924a..b0484ba2f08 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/change-user-password-dialog/change-user-password-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/change-user-password-dialog/change-user-password-dialog.component.html @@ -3,12 +3,14 @@

Change password of {{ user.name }}


@@ -18,12 +20,12 @@

Change password of {{ user.name }}

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html index 9a2db3be13f..0349171050d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html @@ -9,7 +9,8 @@

@@ -19,10 +20,7 @@

 

- + Add members

 

@if (memberItems$ | async; as memberItems) { @@ -53,14 +51,10 @@

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html index 7354b5bb713..ac035ad0565 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html @@ -9,29 +9,29 @@

Scopes

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html index 8ccad17bebd..8c96317238a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html @@ -9,17 +9,23 @@


 

@@ -28,30 +34,29 @@


 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-group/edit-group.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-group/edit-group.component.html index d3903acc6e7..c58836e74e2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-group/edit-group.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-group/edit-group.component.html @@ -15,10 +15,7 @@

 

- + Add members

 

@if (memberItems$ | async; as memberItems) { @@ -47,14 +44,13 @@ }

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-user/edit-user.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-user/edit-user.component.html index ce064f1f324..1f07293029f 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-user/edit-user.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/edit-user/edit-user.component.html @@ -54,10 +54,7 @@

 

- + Add roles

 

@if (roleItems$ | async; as roleItems) {
@@ -83,14 +80,13 @@

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/replication/show-streams-dialog/show-streams-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/replication/show-streams-dialog/show-streams-dialog.component.html index b2dabd7ddd1..a82fbf900a9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/replication/show-streams-dialog/show-streams-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/replication/show-streams-dialog/show-streams-dialog.component.html @@ -11,5 +11,5 @@

Replicated Streams

- + Close diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/acknowledge-alarm-dialog/acknowledge-alarm-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/acknowledge-alarm-dialog/acknowledge-alarm-dialog.component.html index 8bc1bfbd137..4008388f614 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/acknowledge-alarm-dialog/acknowledge-alarm-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/acknowledge-alarm-dialog/acknowledge-alarm-dialog.component.html @@ -21,8 +21,8 @@

Acknowledge

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/action-log-tab/action-log-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/action-log-tab/action-log-tab.component.html index b5026bbd95e..434cbf833a9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/action-log-tab/action-log-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/action-log-tab/action-log-tab.component.html @@ -9,12 +9,10 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-detail/alarm-detail.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-detail/alarm-detail.component.html index 43312a5e927..2ecc965123c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-detail/alarm-detail.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-detail/alarm-detail.component.html @@ -109,26 +109,16 @@ @if (mayControl) {
@if (!alarm.shelveInfo && !alarm.acknowledged) { - + } @if (!alarm.shelveInfo) { - + SHELVE } @if (alarm.shelveInfo) { - + UNSHELVE } - + CLEAR
} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-history/alarm-history.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-history/alarm-history.component.html index d36eddf8f80..46a241b8bd9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-history/alarm-history.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/alarm-history/alarm-history.component.html @@ -10,12 +10,12 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } @@ -153,7 +153,7 @@ - + Load more diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/shelve-alarm-dialog/shelve-alarm-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/shelve-alarm-dialog/shelve-alarm-dialog.component.html index 49c6903fd1f..8ad68db9339 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/shelve-alarm-dialog/shelve-alarm-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/alarms/shelve-alarm-dialog/shelve-alarm-dialog.component.html @@ -14,7 +14,8 @@

Shelve


@@ -26,6 +27,6 @@

Shelve

- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.html index c4b9bf33678..d6c3185c5e3 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-detail/algorithm-detail.component.html @@ -98,21 +98,19 @@
@if (isChangeMissionDatabaseEnabled()) {
- - +
}
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-trace-tab/algorithm-trace-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-trace-tab/algorithm-trace-tab.component.html index ad8bdb5b808..f378a0cfc39 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-trace-tab/algorithm-trace-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/algorithms/algorithm-trace-tab/algorithm-trace-tab.component.html @@ -3,8 +3,10 @@ @if (status.traceEnabled) {
-   -   + Stop trace +   + Refresh +   @if (section$ | async; as section) { - {{ input.id.name }}: + {{ input.id.name }} + + : @if (!last) {
@@ -71,8 +74,9 @@ [routerLink]="'/telemetry/parameters' + output.id.name" [queryParams]="{ c: yamcs.context }" class="secundary"> - {{ output.id.name }}: + {{ output.id.name }} + + : @if (!last) {
@@ -91,26 +95,34 @@ } @else {

Tracing has started.

-

This page does not update automatically.

+

+ This page does + not + update automatically. +

Hit refresh, or check back at a later time when you think there should be trace entries.

- + Refresh

} } @else {

Tracing has started.

-

This page does not update automatically.

+

+ This page does + not + update automatically. +

Hit refresh, or check back at a later time when you think there should be trace entries.

- + Refresh

} @@ -131,26 +143,34 @@ } @else {

Tracing has started.

-

This page does not update automatically.

+

+ This page does + not + update automatically. +

Hit refresh, or check back at a later time when you think there should be trace entries.

- + Refresh

} } @else {

Tracing has started.

-

This page does not update automatically.

+

+ This page does + not + update automatically. +

Hit refresh, or check back at a later time when you think there should be trace entries.

- + Refresh

} @@ -160,7 +180,7 @@

The trace tool captures runs, inputs, outputs and log messages of this algorithm.

- + Start trace

} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/create-instance-page2/create-instance-page2.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/create-instance-page2/create-instance-page2.component.html index 48814a0bbbd..c57b933731d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/create-instance-page2/create-instance-page2.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/create-instance-page2/create-instance-page2.component.html @@ -15,7 +15,8 @@
@for (v of template?.variables; track v) { @@ -51,14 +52,10 @@

 

- - +
} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/server-unavailable/server-unavailable.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/server-unavailable/server-unavailable.component.html index bd5bc827271..17792bf5b70 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/server-unavailable/server-unavailable.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/appbase/server-unavailable/server-unavailable.component.html @@ -7,7 +7,7 @@

Server unavailable

Yamcs appears to be down.

 

- + RELOAD

diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.css b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.css index 351894591ff..a6324ebb16f 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.css +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.css @@ -4,10 +4,6 @@ /*display: none;*/ } -.ya-button.active { - color: #009e87; -} - .timeline-container { line-height: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.html index 73ce9345e38..fa6b3f7caaf 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/archive-browser/archive-browser.component.html @@ -39,74 +39,48 @@
- - - - + (mouseleave)="untoggleMove()" + icon="chevron_right"> +   - + @if ((tool$ | async) === "range-select") { - + } - - - + Today + + Jump to...   - - + +   - - + matTooltip="Use range selection tool" + icon="highlight_alt" />      @for (option of legendOptions; track option) { diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/download-dump-dialog/download-dump-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/download-dump-dialog/download-dump-dialog.component.html index bdca0f155cb..3af8edbbbd9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/download-dump-dialog/download-dump-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/download-dump-dialog/download-dump-dialog.component.html @@ -14,12 +14,12 @@

Download Packet Dump

- + CANCEL + appearance="primary"> DOWNLOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/jump-to-dialog/jump-to-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/jump-to-dialog/jump-to-dialog.component.html index b8feebd1ccd..2d510f3082b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/jump-to-dialog/jump-to-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/jump-to-dialog/jump-to-dialog.component.html @@ -8,6 +8,6 @@

Jump to date

- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/modify-replay-dialog/modify-replay-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/modify-replay-dialog/modify-replay-dialog.component.html index 5617f933dc0..4004f17dd29 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/modify-replay-dialog/modify-replay-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/archive/modify-replay-dialog/modify-replay-dialog.component.html @@ -3,12 +3,14 @@

Configure replay



@@ -16,6 +18,6 @@

Configure replay

- - + CANCEL + SUBMIT diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/change-level-dialog/change-level-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/change-level-dialog/change-level-dialog.component.html index 72d8020f442..f95b963e49b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/change-level-dialog/change-level-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/change-level-dialog/change-level-dialog.component.html @@ -34,8 +34,8 @@

Command clearance

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/clearances-action-log-tab/clearances-action-log-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/clearances-action-log-tab/clearances-action-log-tab.component.html index b6322089aaf..1e31f5160cf 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/clearances-action-log-tab/clearances-action-log-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/clearances/clearances-action-log-tab/clearances-action-log-tab.component.html @@ -9,12 +9,10 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now }
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html index d9940bfe032..de7ee09e3c6 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/command-history-list/command-history-list.component.html @@ -34,12 +34,11 @@

- + }

 

@@ -59,14 +58,15 @@
@if (showCommandExports()) { - + [disabled]="!(dataSource.records$ | async)?.length" + dropdown="true" + icon="download"> + Export + } - + } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } @if (dataSource.loading$ | async) { @@ -456,9 +453,9 @@ - + + Load more + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/export-commands-dialog/export-commands-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/export-commands-dialog/export-commands-dialog.component.html index 57fc3ad8736..d20929dabfa 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/export-commands-dialog/export-commands-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-history/export-commands-dialog/export-commands-dialog.component.html @@ -13,17 +13,18 @@

Export commands


- + CANCEL DOWNLOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/arguments/enumeration-argument/enumeration-argument.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/arguments/enumeration-argument/enumeration-argument.component.html index 2e638eaa2c3..cd9a966b779 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/arguments/enumeration-argument/enumeration-argument.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/arguments/enumeration-argument/enumeration-argument.component.html @@ -12,12 +12,11 @@
-   - + (click)="openSelectEnumerationDialog()" + icon="search">
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/command-report/command-report.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/command-report/command-report.component.html index 0220037b784..4d5964d12c9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/command-report/command-report.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/command-report/command-report.component.html @@ -1,5 +1,5 @@ - Send a command + Send a command @@ -10,19 +10,15 @@

- +    - + } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/configure-command/configure-command.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/configure-command/configure-command.component.html index 4fb2363f398..10428300cb7 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/configure-command/configure-command.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/configure-command/configure-command.component.html @@ -1,5 +1,5 @@ - Send a command + Send a command @@ -41,7 +41,7 @@ } @if (!(cleared$ | async)) { - You're not cleared to send this command + You're not cleared to send this command } @if (command$ | async; as command) { @@ -60,10 +60,10 @@

Constraints

{{ constraint.timeout || "-" }} @if ((expr.result$ | async) === true) { - satisfied + satisfied } @if ((expr.result$ | async) === false) { - not satisfied + not satisfied } Arguments [templateProvider]="templateProvider$ | async" />

 

- + Cancel @if (config.twoStageCommanding) { - Arm + Arm }      - + @if (showSchedule()) {   Send later
- Execution time
+ Execution time +

- Timeline tags (optional) + Timeline tags + (optional) Tags allow to categorise items per band. Bands only show items for which one of the tags is matching. @@ -25,6 +27,6 @@

Send later

- - + CANCEL + SCHEDULE
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/select-enumeration-dialog/select-enumeration-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/select-enumeration-dialog/select-enumeration-dialog.component.html index be89ed178ce..0178fcc1fae 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/select-enumeration-dialog/select-enumeration-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/command-sender/select-enumeration-dialog/select-enumeration-dialog.component.html @@ -46,9 +46,9 @@

Select enumeration state

- + CANCEL - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/queues/queues-action-log-tab/queues-action-log-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/queues/queues-action-log-tab/queues-action-log-tab.component.html index ca83a44e9c4..30b1548b183 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/queues/queues-action-log-tab/queues-action-log-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/queues/queues-action-log-tab/queues-action-log-tab.component.html @@ -4,12 +4,10 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now }
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-dialog/create-stack-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-dialog/create-stack-dialog.component.html index da13a90ad1e..8defc95167f 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-dialog/create-stack-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-dialog/create-stack-dialog.component.html @@ -18,6 +18,6 @@

Create stack

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-folder-dialog/create-stack-folder-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-folder-dialog/create-stack-folder-dialog.component.html index 29cae73fdf3..93bd17a17ea 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-folder-dialog/create-stack-folder-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/create-stack-folder-dialog/create-stack-folder-dialog.component.html @@ -10,6 +10,6 @@

Create folder

- - + CANCEL + CREATE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/edit-stack-entry-dialog/edit-stack-entry-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/edit-stack-entry-dialog/edit-stack-entry-dialog.component.html index ff8aad210e3..cf547da1fe4 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/edit-stack-entry-dialog/edit-stack-entry-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/edit-stack-entry-dialog/edit-stack-entry-dialog.component.html @@ -10,7 +10,7 @@

Select command

@@ -30,7 +30,8 @@

{{ command.name }}

@for (alias of command.alias || []; track alias) {
- {{ alias.namespace }}
+ {{ alias.namespace }} +

{{ alias.name }}

} @@ -86,11 +87,11 @@

Stack advancement

diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/rename-stack-dialog/rename-stack-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/rename-stack-dialog/rename-stack-dialog.component.html index 4ebfddb5cd9..112d12d89b4 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/rename-stack-dialog/rename-stack-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/rename-stack-dialog/rename-stack-dialog.component.html @@ -10,8 +10,8 @@

Rename stack

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/schedule-stack-dialog/schedule-stack-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/schedule-stack-dialog/schedule-stack-dialog.component.html index 20891c66b8b..664778de306 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/schedule-stack-dialog/schedule-stack-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/schedule-stack-dialog/schedule-stack-dialog.component.html @@ -8,12 +8,14 @@

Run later

- Execution time
+ Execution time +

- Timeline tags (optional) + Timeline tags + (optional) Tags allow to categorise items per band. Bands only show items for which one of the tags is matching. @@ -27,6 +29,6 @@

Run later

- - + CANCEL + SCHEDULE
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-file-dirty-guard/stack-file-dirty-guard-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-file-dirty-guard/stack-file-dirty-guard-dialog.component.html index de3c5f93c30..83957fbdae4 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-file-dirty-guard/stack-file-dirty-guard-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-file-dirty-guard/stack-file-dirty-guard-dialog.component.html @@ -4,6 +4,6 @@

Close without saving?

- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-folder/stack-folder.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-folder/stack-folder.component.html index f26e37c55de..7c04e387ba2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-folder/stack-folder.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/commanding/stacks/stack-folder/stack-folder.component.html @@ -196,9 +196,9 @@

@if (mayManageStacks()) {

- + Create a stack or - + Import a stack

} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.html index 53ce2217165..b300abfd065 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-dialog/create-event-dialog.component.html @@ -8,7 +8,8 @@

Create event



@@ -20,6 +21,6 @@

Create event

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html index 074887ac3ac..b6d30950dfb 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/create-event-query-dialog/create-event-query-dialog.component.html @@ -43,6 +43,6 @@

Save query

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html index 39dc57db9a0..8af88ddecd8 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/edit-event-query-dialog/edit-event-query-dialog.component.html @@ -43,6 +43,6 @@

Edit query

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html index 077605cf41f..1ee1eafa413 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/event-list/event-list.component.html @@ -32,17 +32,16 @@ @if (dataSource) {
- - +
@@ -56,9 +55,9 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } Multiline @@ -145,19 +144,13 @@
} - + Export CSV
@@ -239,9 +232,7 @@ - + Load more
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/export-events-dialog/export-events-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/export-events-dialog/export-events-dialog.component.html index c812440718b..ed61ad12b59 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/events/export-events-dialog/export-events-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/events/export-events-dialog/export-events-dialog.component.html @@ -52,10 +52,10 @@

Export events

- + CANCEL DOWNLOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/action-log-tab/action-log-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/action-log-tab/action-log-tab.component.html index cd5374419ce..2ad5c6c5bc2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/action-log-tab/action-log-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/action-log-tab/action-log-tab.component.html @@ -9,12 +9,10 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/file-transfer-list/file-transfer-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/file-transfer-list/file-transfer-list.component.html index f0b34f0a02d..154a6a4102a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/file-transfer-list/file-transfer-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/file-transfer-list/file-transfer-list.component.html @@ -59,12 +59,12 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/transfer-file-dialog/transfer-file-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/transfer-file-dialog/transfer-file-dialog.component.html index a802ddd7df4..5c821b7da6d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/transfer-file-dialog/transfer-file-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/file-transfer/transfer-file-dialog/transfer-file-dialog.component.html @@ -107,10 +107,10 @@

[value]="form.get('localFilenames')?.value" /> @if (service.capabilities.upload) { - + } @@ -187,13 +187,10 @@

diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/initiate-cop1-dialog/initiate-cop1-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/initiate-cop1-dialog/initiate-cop1-dialog.component.html index b92ea5c95a8..de0f6460cf9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/initiate-cop1-dialog/initiate-cop1-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/initiate-cop1-dialog/initiate-cop1-dialog.component.html @@ -30,8 +30,11 @@

Initiate COP-1 AD Service

within this time, it will be retransmitted.

- Other names: T1_Initial  •  - Timer_Initial_Value. + Other names: + T1_Initial +  •  + Timer_Initial_Value + .

@@ -45,7 +48,11 @@

Initiate COP-1 AD Service

The N(S) frame sequence number expected to be seen by FARM-1 in the next Type-AD transfer frame.

-

Other name: Receiver_Frame_Sequence_Number.

+

+ Other name: + Receiver_Frame_Sequence_Number + . +

@@ -55,8 +62,8 @@

Initiate COP-1 AD Service

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-action-dialog/link-action-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-action-dialog/link-action-dialog.component.html index 514efe4a5fd..ce12380cf49 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-action-dialog/link-action-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-action-dialog/link-action-dialog.component.html @@ -25,6 +25,8 @@

{{ data.action.label }}

- - + CANCEL + + SUBMIT + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-detail/link-detail.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-detail/link-detail.component.html index 99dac6bf502..a6b69961742 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-detail/link-detail.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/links/link-detail/link-detail.component.html @@ -41,7 +41,8 @@ @if (getEntriesForValue(entry.value); as subentries) { @if (subentries.length) { @for (subentry of subentries; track subentry) { - {{ subentry || "-" }}
+ {{ subentry || "-" }} +
} } @if (!subentries.length) { @@ -55,15 +56,12 @@ @if (mayControlLinks()) {
@if (link.status === "DISABLED") { - + ENABLE LINK } @if (link.status !== "DISABLED") { - + DISABLE LINK } - + MORE @@ -72,9 +70,9 @@ @for (action of link.actions; track action) { + Close diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/procedures/run-script/run-script.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/procedures/run-script/run-script.component.html index f50a576e2f6..4ce4a543767 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/procedures/run-script/run-script.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/procedures/run-script/run-script.component.html @@ -1,10 +1,12 @@ - Run a script + Run a script
- Script (required)
+ Script + (required) +

@@ -16,13 +18,7 @@

 

- + Run @if (showSchedule()) {   Run later
- Execution time
+ Execution time +

- Timeline tags (optional) + Timeline tags + (optional) Tags allow to categorise items per band. Bands only show items for which one of the tags is matching. @@ -25,6 +27,6 @@

Run later

- - + CANCEL + SCHEDULE
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/search/search.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/search/search.component.html index 574a92cb732..0d47d5d00ac 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/search/search.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/search/search.component.html @@ -13,18 +13,18 @@ @for (resource of result.resources; track resource) { } @if (result.resources.length) { - + + Load more + } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-instance-dialog/select-instance-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-instance-dialog/select-instance-dialog.component.html index d54bc3ee7e9..375938f0f50 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-instance-dialog/select-instance-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-instance-dialog/select-instance-dialog.component.html @@ -44,9 +44,7 @@

Select instance

@if (isCreateInstanceEnabled()) { - - NEW INSTANCE - + NEW INSTANCE } @@ -55,9 +53,9 @@

Select instance

- + CANCEL - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-parameter-dialog/select-parameter-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-parameter-dialog/select-parameter-dialog.component.html index 9510c2df021..debd3763b32 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-parameter-dialog/select-parameter-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/select-parameter-dialog/select-parameter-dialog.component.html @@ -18,8 +18,8 @@

{{ label }}

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/session-expired-dialog/session-expired-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/session-expired-dialog/session-expired-dialog.component.html index 0b0c4203c9d..7aa706cb382 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/session-expired-dialog/session-expired-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/session-expired-dialog/session-expired-dialog.component.html @@ -1,5 +1,5 @@ - Connection to Yamcs was lost. +Connection to Yamcs was lost. - + RELOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/start-replay-dialog/start-replay-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/start-replay-dialog/start-replay-dialog.component.html index 496f5f3215d..f3d6a11380b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/start-replay-dialog/start-replay-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/shared/start-replay-dialog/start-replay-dialog.component.html @@ -3,12 +3,14 @@

Start replay processor



@@ -21,6 +23,6 @@

Start replay processor

- - + CANCEL + START diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-bucket-dialog/create-bucket-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-bucket-dialog/create-bucket-dialog.component.html index 1b9b7121216..eb540208503 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-bucket-dialog/create-bucket-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-bucket-dialog/create-bucket-dialog.component.html @@ -10,6 +10,6 @@

Create bucket

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-folder-dialog/create-folder-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-folder-dialog/create-folder-dialog.component.html index 29cae73fdf3..93bd17a17ea 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-folder-dialog/create-folder-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/create-folder-dialog/create-folder-dialog.component.html @@ -10,6 +10,6 @@

Create folder

- - + CANCEL + CREATE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/rename-object-dialog/rename-object-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/rename-object-dialog/rename-object-dialog.component.html index ef1515b2f99..a1e5b90c03c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/rename-object-dialog/rename-object-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/rename-object-dialog/rename-object-dialog.component.html @@ -10,8 +10,8 @@

Rename file

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/view-object-metadata-dialog/view-object-metadata-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/view-object-metadata-dialog/view-object-metadata-dialog.component.html index c77824a9e94..049a5d486d4 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/view-object-metadata-dialog/view-object-metadata-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/storage/buckets/view-object-metadata-dialog/view-object-metadata-dialog.component.html @@ -16,5 +16,5 @@

Object Metadata

- + CLOSE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-dialog/create-display-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-dialog/create-display-dialog.component.html index 0823d36b0b5..279b7c5c099 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-dialog/create-display-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-dialog/create-display-dialog.component.html @@ -18,7 +18,10 @@

Create display

Name @if (filenameForm.get("name")?.invalid) { - File name must end with .par + + File name must end with + .par + }
@@ -31,13 +34,13 @@

Create display

- + CANCEL @if (page === 1) { - + NEXT » } @if (page === 2) { - + } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-folder-dialog/create-display-folder-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-folder-dialog/create-display-folder-dialog.component.html index 29cae73fdf3..93bd17a17ea 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-folder-dialog/create-display-folder-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/create-display-folder-dialog/create-display-folder-dialog.component.html @@ -10,6 +10,6 @@

Create folder

- - + CANCEL + CREATE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/display-file-dirty-guard/display-file-dirty-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/display-file-dirty-guard/display-file-dirty-dialog.component.html index 7c302131965..8f86624d86d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/display-file-dirty-guard/display-file-dirty-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/display-file-dirty-guard/display-file-dirty-dialog.component.html @@ -3,6 +3,6 @@ - - + CANCEL + DISCARD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/export-archive-data-dialog/export-archive-data-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/export-archive-data-dialog/export-archive-data-dialog.component.html index 99807af2833..4079862fe6d 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/export-archive-data-dialog/export-archive-data-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/export-archive-data-dialog/export-archive-data-dialog.component.html @@ -16,7 +16,9 @@

Export archive data


- Interval (ms) (optional)
+ Interval (ms) + (optional) +
@@ -24,12 +26,14 @@

Export archive data

- CSV column delimiter
+ CSV column delimiter +

- Header
+ Header +
@@ -39,10 +43,10 @@

Export archive data

- + CANCEL DOWNLOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/rename-display-dialog/rename-display-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/rename-display-dialog/rename-display-dialog.component.html index ef1515b2f99..a1e5b90c03c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/rename-display-dialog/rename-display-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/displays/rename-display-dialog/rename-display-dialog.component.html @@ -10,8 +10,8 @@

Rename file

- - + diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/packets/packet-list/packet-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/packets/packet-list/packet-list.component.html index 9af6186a47d..c9753480626 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/packets/packet-list/packet-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/packets/packet-list/packet-list.component.html @@ -78,12 +78,12 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } @@ -167,9 +167,7 @@ - + Load more diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/create-parameter-list/create-parameter-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/create-parameter-list/create-parameter-list.component.html index 8e2ab79b175..fd146a84b2e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/create-parameter-list/create-parameter-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/create-parameter-list/create-parameter-list.component.html @@ -9,7 +9,8 @@

@@ -19,10 +20,9 @@

 

- +

 

@if (patterns$ | async; as patterns) { @@ -55,14 +55,10 @@

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/edit-parameter-list/edit-parameter-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/edit-parameter-list/edit-parameter-list.component.html index e12908dec92..97b1c1b3a8e 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/edit-parameter-list/edit-parameter-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/edit-parameter-list/edit-parameter-list.component.html @@ -1,10 +1,11 @@ @if (plist$ | async; as plist) { - Edit list: {{ plist.name }} + Edit list: {{ plist.name }}

@@ -14,10 +15,9 @@

 

- +

 

@if (patterns$ | async; as patterns) { @@ -48,14 +48,13 @@ }

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/parameter-list-historical-data-tab/parameter-list-historical-data-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/parameter-list-historical-data-tab/parameter-list-historical-data-tab.component.html index 6107e283a2c..c2a534a2c1a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/parameter-list-historical-data-tab/parameter-list-historical-data-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameter-lists/parameter-list-historical-data-tab/parameter-list-historical-data-tab.component.html @@ -7,21 +7,22 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now }
- + @@ -33,29 +34,44 @@ @switch (appliedInterval) { @case ("PT1H") { - Showing data from the last hour ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last hour + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("PT6H") { - Showing data from the last 6 hours ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last 6 hours + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("P1D") { - Showing data from the last 24 hours ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last 24 hours + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("NO_LIMIT") { - Showing data from all time + + Showing data from + all time + } @case ("CUSTOM") { - Showing data from {{ validStart | datetime }} to - {{ validStop | datetime }} (Mission Time) + Showing data from + {{ validStart | datetime }} + to + {{ validStop | datetime }} + (Mission Time) } } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/compare-parameter-dialog/compare-parameter-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/compare-parameter-dialog/compare-parameter-dialog.component.html index ce38f9f18fa..4080f69ebbb 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/compare-parameter-dialog/compare-parameter-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/compare-parameter-dialog/compare-parameter-dialog.component.html @@ -21,6 +21,6 @@ - - + CANCEL + ADD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/export-parameter-data-dialog/export-parameter-data-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/export-parameter-data-dialog/export-parameter-data-dialog.component.html index 7da71cea4d1..e314693f359 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/export-parameter-data-dialog/export-parameter-data-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/export-parameter-data-dialog/export-parameter-data-dialog.component.html @@ -3,32 +3,37 @@

Export parameter data




- + CANCEL DOWNLOAD diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/modify-parameter-dialog/modify-parameter-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/modify-parameter-dialog/modify-parameter-dialog.component.html index d8468c1b520..cece39968ed 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/modify-parameter-dialog/modify-parameter-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/modify-parameter-dialog/modify-parameter-dialog.component.html @@ -1,15 +1,17 @@ -
+ +

-
+ +
- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-alarms-tab/parameter-alarms-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-alarms-tab/parameter-alarms-tab.component.html index 8ffffa089d8..b4a05fe7f7c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-alarms-tab/parameter-alarms-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-alarms-tab/parameter-alarms-tab.component.html @@ -6,13 +6,11 @@ @if (filter.value["interval"] === "CUSTOM") { - + Apply } @if (filter.value["interval"] !== "CUSTOM") { - + Jump to now } @if (dataSource.loading$ | async) { @@ -25,23 +23,35 @@ @switch (appliedInterval) { @case ("P1M") { - Showing data from the last month ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last month + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("P1Y") { - Showing data from the last year ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last year + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("NO_LIMIT") { - Showing data from all time + + Showing data from + all time + } @case ("CUSTOM") { - Showing data from {{ validStart | datetime }} to - {{ validStop | datetime }} (Mission Time) + Showing data from + {{ validStart | datetime }} + to + {{ validStop | datetime }} + (Mission Time) } } @@ -58,9 +68,7 @@ @if (!(dataSource.loading$ | async) && !dataSource.isEmpty()) { - + Load more } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-data-tab/parameter-data-tab.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-data-tab/parameter-data-tab.component.html index 16ab9f3df6a..22d7661e6c2 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-data-tab/parameter-data-tab.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/parameter-data-tab/parameter-data-tab.component.html @@ -1,6 +1,6 @@ @if (table.selectedValue | async; as pval) { - Value Detail + Value Detail
Severity
@@ -43,7 +43,7 @@
} @else { - Select a value + Select a value }
@@ -55,13 +55,11 @@ @if (filterForm.value["interval"] === "CUSTOM") { - + Apply } @if (filterForm.value["interval"] !== "CUSTOM") { - + Jump to now } @if (dataSource.loading$ | async) { @@ -70,10 +68,13 @@
- +
@@ -81,29 +82,44 @@ @switch (appliedInterval) { @case ("PT1H") { - Showing data from the last hour ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last hour + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("PT6H") { - Showing data from the last 6 hours ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last 6 hours + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("P1D") { - Showing data from the last 24 hours ending at - {{ validStop | datetime }} (Mission Time) + Showing data from + the last 24 hours + ending at + {{ validStop | datetime }} + (Mission Time) } @case ("NO_LIMIT") { - Showing data from all time + + Showing data from + all time + } @case ("CUSTOM") { - Showing data from {{ validStart | datetime }} to - {{ validStop | datetime }} (Mission Time) + Showing data from + {{ validStart | datetime }} + to + {{ validStop | datetime }} + (Mission Time) } } @@ -120,9 +136,7 @@ @if (!(dataSource.loading$ | async) && !dataSource.isEmpty()) { - + Load more } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/select-range-dialog/select-range-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/select-range-dialog/select-range-dialog.component.html index 35cec6bd438..fe09587c1ff 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/select-range-dialog/select-range-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/select-range-dialog/select-range-dialog.component.html @@ -14,6 +14,6 @@

Select Range

- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/set-parameter-dialog/set-parameter-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/set-parameter-dialog/set-parameter-dialog.component.html index 8a905f849b7..2db72c09a2a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/set-parameter-dialog/set-parameter-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/telemetry/parameters/set-parameter-dialog/set-parameter-dialog.component.html @@ -7,6 +7,6 @@

Set parameter value

- - + CANCEL + SET diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/command-band/create-command-band/create-command-band.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/command-band/create-command-band/create-command-band.component.html index c4e258322f1..ee424db3630 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/command-band/create-command-band/create-command-band.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/command-band/create-command-band/create-command-band.component.html @@ -1,17 +1,21 @@ - Create band + Create band

@@ -22,16 +26,10 @@

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-item-dialog/create-item-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-item-dialog/create-item-dialog.component.html index 7203257365d..1893f89d6b8 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-item-dialog/create-item-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-item-dialog/create-item-dialog.component.html @@ -6,7 +6,8 @@

Add {{ data.type | lowercase }} item


@@ -28,7 +29,8 @@

Add {{ data.type | lowercase }} item

@@ -44,6 +46,6 @@

Add {{ data.type | lowercase }} item

- - + CANCEL + SAVE diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-view/create-view.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-view/create-view.component.html index 188aa555594..8c3d637ce87 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-view/create-view.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/create-view/create-view.component.html @@ -9,7 +9,9 @@
@@ -23,14 +25,10 @@

Bands

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band-dialog/edit-band-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band-dialog/edit-band-dialog.component.html index ab9c6574a6d..5b1d136bb37 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band-dialog/edit-band-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band-dialog/edit-band-dialog.component.html @@ -23,14 +23,14 @@

Edit Band

diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band/edit-band.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band/edit-band.component.html index dd42ad0ad34..f57d97c287c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band/edit-band.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-band/edit-band.component.html @@ -18,14 +18,13 @@

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item-dialog/edit-item-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item-dialog/edit-item-dialog.component.html index 595277ebcf1..a527cb16f25 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item-dialog/edit-item-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item-dialog/edit-item-dialog.component.html @@ -6,7 +6,8 @@

Update item


@@ -28,7 +29,8 @@

Update item

@@ -44,8 +46,8 @@

Update item

- + DELETE THIS ITEM
- - + CANCEL + SAVE
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item/edit-item.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item/edit-item.component.html index 03890f9fe45..c56bb865f97 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item/edit-item.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-item/edit-item.component.html @@ -4,7 +4,9 @@
- Name (required)
+ Name + (required) +

@@ -28,7 +30,8 @@

- Start
+ Start +

@@ -41,14 +44,13 @@

Styles

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view-dialog/edit-view-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view-dialog/edit-view-dialog.component.html index 3f747071d38..c00411a8ac9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view-dialog/edit-view-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view-dialog/edit-view-dialog.component.html @@ -2,7 +2,9 @@

Edit View

@@ -19,13 +21,14 @@

Bands

diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view/edit-view.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view/edit-view.component.html index b681c459864..de252493678 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view/edit-view.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/edit-view/edit-view.component.html @@ -9,7 +9,9 @@
@@ -23,14 +25,13 @@

Bands

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/item-band/create-item-band/create-item-band.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/item-band/create-item-band/create-item-band.component.html index b99ad2e80f8..0ae526d22f9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/item-band/create-item-band/create-item-band.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/item-band/create-item-band/create-item-band.component.html @@ -1,22 +1,27 @@ - Create band + Create band


@@ -30,16 +35,10 @@

Styles

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/jump-to-dialog/jump-to-dialog.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/jump-to-dialog/jump-to-dialog.component.html index b8feebd1ccd..2d510f3082b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/jump-to-dialog/jump-to-dialog.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/jump-to-dialog/jump-to-dialog.component.html @@ -8,6 +8,6 @@

Jump to date

- - + CANCEL + OK diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/shared/band-multi-select/band-multi-select.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/shared/band-multi-select/band-multi-select.component.html index 3b6c170ad78..94aafc686e9 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/shared/band-multi-select/band-multi-select.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/shared/band-multi-select/band-multi-select.component.html @@ -27,20 +27,15 @@
-
- + + Add + chevron_right + +
+ + chevron_left + Remove +
@@ -71,10 +66,6 @@
- - + +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/spacer/create-spacer/create-spacer.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/spacer/create-spacer/create-spacer.component.html index 898cfc749b8..7c072b6ad9b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/spacer/create-spacer/create-spacer.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/spacer/create-spacer/create-spacer.component.html @@ -1,17 +1,21 @@ - Create band + Create band

@@ -25,16 +29,10 @@

Styles

 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/time-ruler/create-time-ruler/create-time-ruler.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/time-ruler/create-time-ruler/create-time-ruler.component.html index 77d197fcb4e..693dddc40da 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/time-ruler/create-time-ruler/create-time-ruler.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/time-ruler/create-time-ruler/create-time-ruler.component.html @@ -1,39 +1,38 @@ - Create band + Create band


 

- - +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/timeline-chart/timeline-chart.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/timeline-chart/timeline-chart.component.html index dbb77dc5a8d..96567c1425c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/timeline-chart/timeline-chart.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/timeline/timeline-chart/timeline-chart.component.html @@ -71,45 +71,25 @@
- - - - + (mouseleave)="untoggleMove()" + icon="chevron_right" /> +   - - - + Today + + Jump to...   - - + +
From 543db08e6502533d60070577da45ba7b500e88fc Mon Sep 17 00:00:00 2001 From: Fabian Diet Date: Fri, 13 Sep 2024 13:50:05 +0200 Subject: [PATCH 16/31] Make page-button component --- .../page-button/page-button.component.html | 11 ++ .../page-button/page-button.component.ts | 29 ++++ .../page-icon-button.component.html | 5 + .../page-icon-button.component.ts | 28 ++++ .../webapp-sdk/src/lib/webapp-sdk.module.ts | 4 + .../projects/webapp-sdk/src/public-api.ts | 2 + .../activity-list.component.html | 10 +- .../activity/activity.component.html | 36 ++--- .../database/database.component.html | 4 +- .../create-group/create-group.component.html | 4 +- .../create-service-account.component.html | 4 +- .../create-user/create-user.component.html | 4 +- .../iam/group-list/group-list.component.html | 9 +- .../app/admin/iam/group/group.component.html | 11 +- .../service-account-list.component.html | 7 +- .../iam/user-list/user-list.component.html | 13 +- .../app/admin/iam/user/user.component.html | 19 +-- .../rocksdb-database.component.html | 11 +- .../route-list/route-list.component.html | 8 +- .../service-list/service-list.component.html | 4 +- .../threads/thread/thread.component.html | 4 +- .../alarm-list/alarm-list.component.html | 24 ++-- .../create-instance-page1.component.html | 4 +- .../create-instance-page2.component.html | 4 +- .../src/app/appbase/home/home.component.html | 47 +++---- .../archive-browser.component.html | 34 ++--- .../clearances-list.component.html | 10 +- .../command-history-list.component.html | 22 ++- .../command/command.component.html | 4 +- .../queues-list/queues-list.component.html | 27 ++-- .../stack-file/stack-file.component.html | 127 +++++++----------- .../stack-folder/stack-folder.component.html | 25 ++-- .../event-list/event-list.component.html | 23 +--- .../file-transfer-list.component.html | 27 ++-- .../links/link-list/link-list.component.html | 64 ++++----- .../src/app/links/link/link.component.html | 43 +++--- .../instance-toolbar.component.html | 25 ++-- .../bucket-list/bucket-list.component.html | 21 ++- .../bucket-object-list.component.html | 35 ++--- .../bucket-properties.component.html | 4 +- .../display-file/display-file.component.html | 7 +- .../display-folder.component.html | 25 ++-- ...opi-display-viewer-controls.component.html | 2 +- ...meter-table-viewer-controls.component.html | 57 ++++---- .../script-viewer-controls.component.html | 9 +- .../packet-list/packet-list.component.html | 14 +- .../packets/packet/packet.component.html | 35 ++--- .../create-parameter-list.component.html | 4 +- .../parameter-list-list.component.html | 10 +- .../parameter-list.component.html | 9 +- .../parameter/parameter.component.html | 21 ++- .../band-list/band-list.component.html | 18 +-- .../create-band/create-band.component.html | 4 +- .../create-view/create-view.component.html | 4 +- .../edit-view/edit-view.component.html | 4 +- .../item-list/item-list.component.html | 15 +-- .../timeline-chart.component.html | 31 ++--- .../view-list/view-list.component.html | 18 +-- 58 files changed, 469 insertions(+), 615 deletions(-) create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.ts create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.html create mode 100644 yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.ts diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.html new file mode 100644 index 00000000000..9d019fdcef4 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.html @@ -0,0 +1,11 @@ + diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.ts new file mode 100644 index 00000000000..bb1f57689c1 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-button/page-button.component.ts @@ -0,0 +1,29 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + standalone: true, + selector: 'ya-page-button', + templateUrl: './page-button.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatButton, + MatIcon, + ], +}) +export class YaPageButton { + + icon = input(); + iconRotate90 = input(false, { transform: booleanAttribute }); + disabled = input(false, { transform: booleanAttribute }); + dropdown = input(false, { transform: booleanAttribute }); + color = input('primary'); + + click = output(); + + onClick(event: MouseEvent) { + this.click.emit(event); + event.stopPropagation(); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.html b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.html new file mode 100644 index 00000000000..e1de563895e --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.html @@ -0,0 +1,5 @@ + diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.ts new file mode 100644 index 00000000000..b93678c79c6 --- /dev/null +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/components/page-icon-button/page-icon-button.component.ts @@ -0,0 +1,28 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + standalone: true, + selector: 'ya-page-icon-button', + templateUrl: './page-icon-button.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatIconButton, + MatIcon, + ], +}) +export class YaPageIconButton { + + icon = input.required(); + iconRotate90 = input(false, { transform: booleanAttribute }); + disabled = input(false, { transform: booleanAttribute }); + color = input('primary'); + + click = output(); + + onClick(event: MouseEvent) { + this.click.emit(event); + event.stopPropagation(); + } +} diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts index e1ee5c71d8c..67f70381e80 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/lib/webapp-sdk.module.ts @@ -54,6 +54,8 @@ import { YaMessageBar } from './components/message-bar/message-bar.component'; import { YaMore } from './components/more/more.component'; import { YaMultiSelect } from './components/multi-select/multi-select.component'; import { YaOption } from './components/option/option.component'; +import { YaPageButton } from './components/page-button/page-button.component'; +import { YaPageIconButton } from './components/page-icon-button/page-icon-button.component'; import { YaProgress } from './components/progress/progress.component'; import { YaSearchFilter } from './components/search-filter/search-filter.component'; import { YaSearchFilter2 } from './components/search-filter2/search-filter2.component'; @@ -179,6 +181,8 @@ const sharedComponents = [ YaMore, YaMultiSelect, YaOption, + YaPageButton, + YaPageIconButton, YaProgress, YaPrintZone, YaSearchFilter, diff --git a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts index c64ce2377ea..f4c2fdb53dc 100644 --- a/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts +++ b/yamcs-web/src/main/webapp/projects/webapp-sdk/src/public-api.ts @@ -36,6 +36,8 @@ export * from './lib/components/message-bar/message-bar.component'; export * from './lib/components/more/more.component'; export * from './lib/components/multi-select/multi-select.component'; export * from './lib/components/option/option.component'; +export * from './lib/components/page-button/page-button.component'; +export * from './lib/components/page-icon-button/page-icon-button.component'; export * from './lib/components/progress/progress.component'; export * from './lib/components/search-filter/search-filter.component'; export * from './lib/components/search-filter2/search-filter2.component'; diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html index a554e93d597..45edbd5e3f8 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity-list/activity-list.component.html @@ -3,14 +3,12 @@ Activities     @if (mayControlActivities()) { - + } diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity/activity.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity/activity.component.html index 3d7379c8bb1..c456e9586ba 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity/activity.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/activities/activity/activity.component.html @@ -1,39 +1,31 @@ @if (activity$ | async; as activity) { - - arrow_back - + Activity details     @if (activity.type === "MANUAL" && activity.status === "RUNNING" && mayControlActivities()) { - - + + Set failed } @if (mayControlActivities()) { - + }
- {{ activity.detail }}
- ID: {{ activity.id }}
- Started {{ activity.start | datetime }}
+ {{ activity.detail }} +
+ ID: {{ activity.id }} +
+ Started {{ activity.start | datetime }} +
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/database/database.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/database/database.component.html index 0825ca0f5c6..351ec4ac03b 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/database/database.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/databases/database/database.component.html @@ -1,9 +1,7 @@ @if (database$ | async; as database) { - - account_tree - + navigate_next {{ database.name }} diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html index 0349171050d..b6c4c5f7ffa 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-group/create-group.component.html @@ -1,8 +1,6 @@ - - arrow_back - + Create group diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html index ac035ad0565..ef9c272a69f 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-service-account/create-service-account.component.html @@ -1,8 +1,6 @@ - - arrow_back - + Create service account diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html index 8c96317238a..0604882131c 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/create-user/create-user.component.html @@ -1,8 +1,6 @@ - - arrow_back - + Create user diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group-list/group-list.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group-list/group-list.component.html index 28e135fcaa2..b4c1d3c7d2a 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group-list/group-list.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group-list/group-list.component.html @@ -1,10 +1,7 @@ Groups     - - add_circle_outline - Create group - + Create group
@@ -41,7 +38,7 @@
@@ -51,7 +48,7 @@
- Edit group + Edit group
@if (!dataSource.data.length) { - No rows to display + No rows to display }
diff --git a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group/group.component.html b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group/group.component.html index c9a5b410cb3..0817d92fb66 100644 --- a/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group/group.component.html +++ b/yamcs-web/src/main/webapp/projects/webapp/src/app/admin/iam/group/group.component.html @@ -3,10 +3,7 @@ {{ group.name }}     - - edit - Edit group - + Edit group