fprime-gds 3.3.2__py3-none-any.whl → 3.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. fprime_gds/common/communication/framing.py +18 -13
  2. fprime_gds/common/communication/ground.py +1 -1
  3. fprime_gds/common/communication/updown.py +26 -5
  4. fprime_gds/common/data_types/event_data.py +9 -3
  5. fprime_gds/common/files/downlinker.py +19 -6
  6. fprime_gds/common/files/helpers.py +1 -0
  7. fprime_gds/common/logger/__init__.py +11 -2
  8. fprime_gds/common/pipeline/files.py +8 -1
  9. fprime_gds/executables/cli.py +18 -8
  10. fprime_gds/executables/comm.py +53 -26
  11. fprime_gds/executables/run_deployment.py +5 -5
  12. fprime_gds/flask/app.py +21 -7
  13. fprime_gds/flask/default_settings.py +4 -2
  14. fprime_gds/flask/json.py +48 -43
  15. fprime_gds/flask/static/addons/advanced-settings/addon-templates.js +4 -0
  16. fprime_gds/flask/static/addons/advanced-settings/addon.js +13 -7
  17. fprime_gds/flask/static/addons/channel-render/channel-render.js +8 -2
  18. fprime_gds/flask/static/addons/commanding/argument-templates.js +14 -3
  19. fprime_gds/flask/static/addons/commanding/arguments.js +53 -6
  20. fprime_gds/flask/static/js/datastore.js +25 -4
  21. fprime_gds/flask/static/js/settings.js +2 -1
  22. fprime_gds/flask/static/js/validate.js +5 -0
  23. fprime_gds/flask/static/js/vue-support/event.js +1 -1
  24. fprime_gds/flask/static/js/vue-support/log.js +21 -13
  25. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/METADATA +2 -2
  26. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/RECORD +31 -31
  27. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/WHEEL +1 -1
  28. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/LICENSE.txt +0 -0
  29. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/NOTICE.txt +0 -0
  30. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/entry_points.txt +0 -0
  31. {fprime_gds-3.3.2.dist-info → fprime_gds-3.4.0.dist-info}/top_level.txt +0 -0
fprime_gds/flask/json.py CHANGED
@@ -20,7 +20,7 @@ from fprime_gds.common.templates.data_template import DataTemplate
20
20
 
21
21
 
22
22
  def jsonify_base_type(input_type: Type[BaseType]) -> dict:
23
- """ Turn a base type into a JSONable dictionary
23
+ """Turn a base type into a JSONable dictionary
24
24
 
25
25
  Convert a BaseType (the type, not an instance) into a jsonable dictionary. BaseTypes are converted by reading the
26
26
  class properties (without __) and creating the object:
@@ -36,14 +36,17 @@ def jsonify_base_type(input_type: Type[BaseType]) -> dict:
36
36
  json-able dictionary representing the type
37
37
  """
38
38
  assert issubclass(input_type, BaseType), "Failure to properly encode data"
39
- members = getmembers(input_type, lambda value: not isroutine(value) and not isinstance(value, property))
39
+ members = getmembers(
40
+ input_type,
41
+ lambda value: not isroutine(value) and not isinstance(value, property),
42
+ )
40
43
  jsonable_dict = {name: value for name, value in members if not name.startswith("_")}
41
44
  jsonable_dict.update({"name": input_type.__name__})
42
45
  return jsonable_dict
43
46
 
44
47
 
45
48
  def getter_based_json(obj):
46
- """ Converts objects to JSON via get_ methods
49
+ """Converts objects to JSON via get_ methods
47
50
 
48
51
  Template functions define a series of get_* methods whose return values need to be serialized. This function
49
52
  handles that data.
@@ -80,7 +83,7 @@ def getter_based_json(obj):
80
83
 
81
84
 
82
85
  def minimal_event(obj):
83
- """ Minimal event encoding: time, id, display_text
86
+ """Minimal event encoding: time, id, display_text
84
87
 
85
88
  Events need time, id, display_text. No other information from the event is necessary for the display. This will
86
89
  minimally encode the data for JSON.
@@ -95,7 +98,7 @@ def minimal_event(obj):
95
98
 
96
99
 
97
100
  def minimal_channel(obj):
98
- """ Minimal channel serialization: time, id, val, and display_text
101
+ """Minimal channel serialization: time, id, val, and display_text
99
102
 
100
103
  Minimally serializes channel values for use with the flask layer. This does away with any unnecessary data by
101
104
  serializing only the id, value, and optional display text
@@ -106,11 +109,16 @@ def minimal_channel(obj):
106
109
  Returns:
107
110
  JSON compatible python anonymous type (dictionary)
108
111
  """
109
- return {"time": obj.time, "id": obj.id, "val": obj.val_obj.val, "display_text": obj.display_text}
112
+ return {
113
+ "time": obj.time,
114
+ "id": obj.id,
115
+ "val": obj.val_obj.val,
116
+ "display_text": obj.display_text,
117
+ }
110
118
 
111
119
 
112
120
  def minimal_command(obj):
113
- """ Minimal command serialization: time, id, and args values
121
+ """Minimal command serialization: time, id, and args values
114
122
 
115
123
  Minimally serializes the command values for use with the flask layer. This prevents excess data by keeping the data
116
124
  to the minimum instance data for commands including: time, opcode (id), and the value for args.
@@ -125,7 +133,7 @@ def minimal_command(obj):
125
133
 
126
134
 
127
135
  def time_type(obj):
128
- """ Time type serialization
136
+ """Time type serialization
129
137
 
130
138
  Serializes the time type into a JSON compatible object.
131
139
 
@@ -137,49 +145,46 @@ def time_type(obj):
137
145
  """
138
146
  assert isinstance(obj, TimeType), "Incorrect type for serialization method"
139
147
  return {
140
- "base": obj.timeBase.value,
141
- "context": obj.timeContext,
142
- "seconds": obj.seconds,
143
- "microseconds": obj.useconds
144
- }
148
+ "base": obj.timeBase.value,
149
+ "context": obj.timeContext,
150
+ "seconds": obj.seconds,
151
+ "microseconds": obj.useconds,
152
+ }
145
153
 
146
154
 
147
155
  def enum_json(obj):
148
- """ Jsonify the python enums! """
156
+ """Jsonify the python enums!"""
149
157
  enum_dict = {"value": str(obj), "values": {}}
150
158
  for enum_val in type(obj):
151
159
  enum_dict["values"][str(enum_val)] = enum_val.value
152
160
  return enum_dict
153
161
 
154
162
 
155
- class GDSJsonEncoder(flask.json.JSONEncoder):
156
- """
157
- Custom class used to handle GDS object to JSON
163
+ JSON_ENCODERS = {
164
+ ABCMeta: jsonify_base_type,
165
+ UUID: str,
166
+ ChData: minimal_channel,
167
+ EventData: minimal_event,
168
+ CmdData: minimal_command,
169
+ TimeType: time_type,
170
+ }
171
+
172
+
173
+ def default(obj):
158
174
  """
159
- JSON_ENCODERS = {
160
- ABCMeta: jsonify_base_type,
161
- UUID: str,
162
- ChData: minimal_channel,
163
- EventData: minimal_event,
164
- CmdData: minimal_command,
165
- TimeType: time_type
166
- }
175
+ Override the default JSON encoder to pull out a dictionary for our handled types for encoding with the default
176
+ encoder built into flask. This function must convert the given object into a JSON compatable python object (e.g.
177
+ using lists, dictionaries, strings, and primitive types).
167
178
 
168
- def default(self, obj):
169
- """
170
- Override the default JSON encoder to pull out a dictionary for our handled types for encoding with the default
171
- encoder built into flask. This function must convert the given object into a JSON compatable python object (e.g.
172
- using lists, dictionaries, strings, and primitive types).
173
-
174
- :param obj: obj to encode
175
- :return: JSON
176
- """
177
- if type(obj) in self.JSON_ENCODERS:
178
- return self.JSON_ENCODERS[type(obj)](obj)
179
- if isinstance(obj, DataTemplate):
180
- return getter_based_json(obj)
181
- if isinstance(obj, Enum):
182
- return enum_json(obj)
183
- if isinstance(obj, ValueType):
184
- return obj.val
185
- return flask.json.JSONEncoder.default(self, obj)
179
+ :param obj: obj to encode
180
+ :return: JSON
181
+ """
182
+ if type(obj) in JSON_ENCODERS:
183
+ return JSON_ENCODERS[type(obj)](obj)
184
+ if isinstance(obj, DataTemplate):
185
+ return getter_based_json(obj)
186
+ if isinstance(obj, Enum):
187
+ return enum_json(obj)
188
+ if isinstance(obj, ValueType):
189
+ return obj.val
190
+ return flask.json.provider.DefaultJSONProvider.default(obj)
@@ -9,6 +9,10 @@ export let advanced_template = `
9
9
  <h3>{{ setting_category.replace("_", " ") }}</h3>
10
10
  <div v-html="settings[setting_category].description"></div>
11
11
  <div class="input-group mb-3" v-for="setting_key in Object.keys(settings[setting_category].settings)">
12
+ <small v-if="(settings[setting_category].descriptions || {})[setting_key]">
13
+ <strong>{{ setting_key }}</strong>
14
+ {{ settings[setting_category].descriptions[setting_key]}}
15
+ </small>
12
16
  <div class="input-group-prepend col-6">
13
17
  <span class="input-group-text col-12">{{ setting_key }}</span>
14
18
  </div>
@@ -19,13 +19,19 @@ Vue.component("advanced-settings", {
19
19
  settings: _settings.polling_intervals
20
20
  },
21
21
  "Miscellaneous": {
22
- description: "Miscellaneous settings for GDS UI operations." +
23
- "<small><ul>" +
24
- "<li>event_buffer_size: maximum event records. Default: -1, infinite.</li>" +
25
- "<li>command_buffer_size: maximum command history records. Default: -1, infinite.</li>" +
26
- "<li>response_object_limit: maximum results to load per polling request. Default: 6000</li>" +
27
- '<li>compact_commanding: use compact "flattened" style for commanding complex arguments.</li>' +
28
- "</ul></small>",
22
+ description: "Miscellaneous settings for GDS UI operations.",
23
+ descriptions: {
24
+ event_buffer_size: "Maximum number of events stored by the GDS. When exceeded, oldest events are dropped " +
25
+ "Lower this value if performance drops on the Events tab. Default: -1, no limit.",
26
+ command_buffer_size: "Maximum number of commands stored by the GDS. When exceeded, oldest commands are dropped " +
27
+ "Lower this value if performance drops on the Commanding tab. Default: -1, no limit.",
28
+ response_object_limit: "Limit to the number of objects returned by one POLL request to the backend. " +
29
+ "Lower this value if polling times are longer than polling intervals. Default: 6000.",
30
+ compact_commanding: "Use the compact form for command arguments. In this form, Array and Serializable type " +
31
+ "inputs are flattened into a sequential set of input boxes without extraneous structure.",
32
+ channels_display_last_received: "When set, any channel received will update the displayed value. Otherwise " +
33
+ "only channels with newer timestamps update the displayed value."
34
+ },
29
35
  settings: _settings.miscellaneous
30
36
  }
31
37
  },
@@ -107,7 +107,13 @@ Vue.component("channel-render", {
107
107
  * @returns: display text of item/child item
108
108
  */
109
109
  displayText() {
110
- return this.item?.display_text || this.item?.val || this.val || "";
110
+ let possibles = [this.item?.display_text, this.item?.val, this.val];
111
+ for (let i = 0; i < possibles.length; i++) {
112
+ if (typeof(possibles[i]) !== "undefined" && possibles[i] !== null) {
113
+ return possibles[i];
114
+ }
115
+ }
116
+ return "";
111
117
  }
112
118
 
113
119
  }
@@ -120,4 +126,4 @@ let channel_row_data = {
120
126
  /**
121
127
  * channel-row replaces fp-row in display template only.
122
128
  */
123
- Vue.component("channel-row", channel_row_data);
129
+ Vue.component("channel-row", channel_row_data);
@@ -15,6 +15,17 @@ export let command_enum_argument_template = `
15
15
  </v-select>
16
16
  `;
17
17
 
18
+ /**
19
+ * Enum argument uses the v-select dropdown to render the various choices while providing search and match capabilities.
20
+ */
21
+ export let command_bool_argument_template = `
22
+ <v-select :id="argument.name" style="flex: 1 1 auto; background-color: white;"
23
+ :clearable="false" :searchable="true" @input="validateTrigger"
24
+ :filterable="true" label="full_name" :options="['True', 'False']"
25
+ v-model="argument.value" class="fprime-input" :class="argument.error == '' ? '' : 'is-invalid'" required>
26
+ </v-select>
27
+ `;
28
+
18
29
  /**
19
30
  * Serializable arguments "flatten" the structure into a list of fields.
20
31
  */
@@ -59,13 +70,13 @@ export let command_array_argument_template = `
59
70
  * enumerations are handled here as they represent a single scalar input.
60
71
  */
61
72
  export let command_scalar_argument_template = `
62
- <div style="display: contents;">
73
+ <div style="display: contents;" class="fprime-scalar-argument">
63
74
  <div class="form-group col-md-6">
64
75
  <label :for="argument.name" class="control-label font-weight-bold">
65
76
  {{ argument.name + ((argument.description != null) ? ": " + argument.description : "") }}
66
77
  </label>
67
-
68
- <command-enum-argument v-if="argument.type.ENUM_DICT" :argument="argument"></command-enum-argument>
78
+ <command-bool-argument v-if="argument.type.name == 'BoolType'" :argument="argument"></command-bool-argument>
79
+ <command-enum-argument v-else-if="argument.type.ENUM_DICT" :argument="argument"></command-enum-argument>
69
80
  <input v-else :type="inputType[0]" v-bind:id="argument.name" class="form-control fprime-input"
70
81
  :placeholder="argument.name" :pattern="inputType[1]" :step="inputType[2]" v-on:input="validateTrigger"
71
82
  v-model="argument.value" :class="argument.error == '' ? '' : 'is-invalid'" required>
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import "../../third-party/js/vue-select.js"
10
10
  import {
11
+ command_bool_argument_template,
11
12
  command_enum_argument_template,
12
13
  command_array_argument_template,
13
14
  command_serializable_argument_template,
@@ -118,6 +119,35 @@ export function squashify_argument(argument) {
118
119
  let field = argument.type.MEMBER_LIST[i][0];
119
120
  value[field] = squashify_argument(argument.value[field]);
120
121
  }
122
+ } else if (["U64Type", "U32Type", "U16Type", "U8Type"].indexOf(argument.type.name) != -1) {
123
+ if (argument.value.startsWith("0x")) {
124
+ // Hexadecimal
125
+ value = parseInt(argument.value, 16);
126
+ } else if (argument.value.startsWith("0b")) {
127
+ // Binary
128
+ value = parseInt(argument.value.slice(2), 2);
129
+ } else if (argument.value.startsWith("0o")) {
130
+ // Octal
131
+ value = parseInt(argument.value.slice(2), 8);
132
+ } else {
133
+ // Decimal
134
+ value = parseInt(argument.value, 10);
135
+ }
136
+ }
137
+ else if (["I64Type", "I32Type", "I16Type", "I8Type"].indexOf(argument.type.name) != -1) {
138
+ value = parseInt(argument.value, 10);
139
+ }
140
+ else if (["F64Type", "F32Type"].indexOf(argument.type.name) != -1) {
141
+ value = parseFloat(argument.value);
142
+ }
143
+ else if (argument.type.name == "BoolType") {
144
+ if ((typeof(value) === "string") && (["true", "yes"].indexOf(value.toLowerCase()) !== -1)) {
145
+ value = true;
146
+ } else if ((typeof(value) === "string") && (["false", "no"].indexOf(value.toLowerCase()) !== -1)) {
147
+ value = false;
148
+ } else {
149
+ console.assert(typeof(value) !== "boolean", "Cannot process boolean, invalid input type")
150
+ }
121
151
  }
122
152
  return value;
123
153
  }
@@ -174,13 +204,22 @@ let base_argument_component_properties = {
174
204
  validateArgument(recurse_down) {
175
205
  recurse_down = !!(recurse_down); // Force recurse_down to be defined as a boolean
176
206
  let valid = validate_input(this.argument);
177
- // HTML element validation
207
+ // Each scalar argument needs to set custom validity on the HTML input that it owns. However, non-scalar
208
+ // inputs skip this step less the first scalar child's input box be poisoned with incorrect validity due
209
+ // to the unbounded recursive nature of getElementsByClassName used to find a fprime-input children.
210
+ let is_scalar = [...document.getElementsByClassName("fprime-scalar-argument")]
211
+ .filter((scalar) => scalar === this.$el || scalar.contains(this.$el)).length > 0;
212
+
213
+ // Now grab the singular the nearest input and report validity if and only if this is a scalar. Non-
214
+ // scalar values are compositions of scalar children and thus validity will be set when the scalar
215
+ // itself is validated.
178
216
  let input_element = this.$el.getElementsByClassName("fprime-input")[0] || this.$el;
179
- if (input_element.setCustomValidity && input_element.reportValidity) {
217
+ if (is_scalar && input_element.setCustomValidity && input_element.reportValidity) {
180
218
  input_element.setCustomValidity(this.argument.error);
181
219
  input_element.reportValidity();
182
220
  }
183
- // Downward recursion uses children
221
+ // Validation can happen recursively down through the children, or up through the parent in order to
222
+ // laterally validate complex arguments when a single input scalar filed is adjusted.
184
223
  let recursive_listing = (recurse_down) ? this.$children.slice().reverse() : [this.$parent];
185
224
  let valid_recursion = (recursive_listing || []).reduce(
186
225
  (accumulator, next_element) => {
@@ -225,6 +264,14 @@ Vue.component("command-enum-argument", {
225
264
  template: command_enum_argument_template,
226
265
  });
227
266
 
267
+ /**
268
+ * Special boolean processing component to render as a drop-down.
269
+ */
270
+ Vue.component("command-bool-argument", {
271
+ ...base_argument_component_properties,
272
+ template: command_bool_argument_template,
273
+ });
274
+
228
275
  /**
229
276
  * Scalar argument processing. Sets up the input type such that numbers can be input with the correct formatting.
230
277
  */
@@ -239,14 +286,14 @@ Vue.component("command-scalar-argument", {
239
286
  */
240
287
  inputType() {
241
288
  // Unsigned integer
242
- if (this.argument.type.name[0] == 'U') {
289
+ if (["U64Type", "U32Type", "U16Type", "U8Type"].indexOf(this.argument.type.name) != -1) {
243
290
  // Supports binary, hex, octal, and digital
244
291
  return ["text", "0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|[1-9]\\d*|0", ""];
245
292
  }
246
- else if (this.argument.type.name[0] == 'I') {
293
+ else if (["I64Type", "I32Type", "I16Type", "I8Type"].indexOf(this.argument.type.name) != -1) {
247
294
  return ["number", null, "1"];
248
295
  }
249
- else if (this.argument.type.name[0] == 'F') {
296
+ else if (["F64Type", "F32Type"].indexOf(this.argument.type.name) != -1) {
250
297
  return ["number", null, "any"];
251
298
  }
252
299
  return ["text", ".*", null];
@@ -28,6 +28,7 @@ class HistoryHelper {
28
28
  this.active_key = active_key;
29
29
  this.consumers = [];
30
30
  this.active_timeout = null;
31
+ this.counter = 0;
31
32
  }
32
33
 
33
34
  /**
@@ -58,7 +59,8 @@ class HistoryHelper {
58
59
  // Break our when no new items returned
59
60
  if (new_items.length === 0) { return; }
60
61
  new_items.filter((item) => item.time).forEach((item) => {
61
- item.datetime = timeToDate(item.time)
62
+ item.datetime = timeToDate(item.time);
63
+ item.incremental_id = this.counter++;
62
64
  });
63
65
  this.consumers.forEach((consumer) => {
64
66
  try {
@@ -119,7 +121,18 @@ class FullListHistory extends ListHistory {
119
121
  * @param new_items: new items being to be process
120
122
  */
121
123
  send(new_items) {
122
- this.store.splice(0, this.store.length, ...new_items);
124
+ // When the lists are not the same, update the stored list otherwise keep the list to prevent unnecessary bound
125
+ // data re-rendering.
126
+ if (this.store.length !== new_items.length) {
127
+ this.store.splice(0, this.store.length, ...new_items);
128
+ return;
129
+ }
130
+ for (let i = 0; i < Math.min(this.store.length, new_items.length); i++) {
131
+ if (this.store[i] !== new_items[i]) {
132
+ this.store.splice(0, this.store.length, ...new_items);
133
+ return;
134
+ }
135
+ }
123
136
  }
124
137
  }
125
138
 
@@ -148,8 +161,12 @@ class MappedHistory extends HistoryHelper {
148
161
  let updated = {};
149
162
  for (let i = 0; i < new_items.length; i++) {
150
163
  let item = new_items[i];
151
- // Check for miss-ordered updates
152
- if ((this.store[item.id] || null) === null || item.datetime >= this.store[item.id].datetime) {
164
+ // When displaying last received, update the value always
165
+ if (_settings.miscellaneous.channels_display_last_received) {
166
+ updated[item.id] = item;
167
+ }
168
+ // Otherwise check for a newer timestamp
169
+ else if ((this.store[item.id] || null) === null || item.datetime >= this.store[item.id].datetime) {
153
170
  updated[item.id] = item;
154
171
  }
155
172
  }
@@ -270,6 +287,10 @@ class DataStore {
270
287
  if (argument.type.ENUM_DICT) {
271
288
  argument.value = Object.keys(argument.type.ENUM_DICT)[0];
272
289
  }
290
+ // Booleans are initialized to True
291
+ else if (argument.type.name === "BoolType") {
292
+ argument.value = "True";
293
+ }
273
294
  // Arrays expand to a set length of N pseudo-arguments
274
295
  else if (argument.type.LENGTH) {
275
296
  let array_length = argument.type.LENGTH;
@@ -12,7 +12,8 @@ class Settings {
12
12
  event_buffer_size: -1,
13
13
  command_buffer_size: -1,
14
14
  response_object_limit: 6000,
15
- compact_commanding: false
15
+ compact_commanding: false,
16
+ channels_display_last_received: true
16
17
  };
17
18
  this.polling_intervals = {};
18
19
  }
@@ -116,6 +116,11 @@ export function validate_scalar_input(argument) {
116
116
  argument.error = "";
117
117
  return true;
118
118
  }
119
+ // Boolean type handling
120
+ else if (argument.type.name === "BoolType") {
121
+ return (argument.value == null) ? null :
122
+ ["yes", "no", "true", "false"].indexOf(argument.value.toString().toLowerCase()) >= 0;
123
+ }
119
124
  console.assert(false, "Unknown scalar type: " + argument.type.name);
120
125
  argument.error = "";
121
126
  return true;
@@ -109,7 +109,7 @@ Vue.component("event-list", {
109
109
  * @return {string} unique key
110
110
  */
111
111
  keyify(item) {
112
- return "evt-" + item.id + "-" + item.time.seconds + "-"+ item.time.microseconds;
112
+ return "evt-" + item.id + "-" + item.time.seconds + "-"+ item.time.microseconds + "-" + item.incremental_id;
113
113
  },
114
114
  /**
115
115
  * A function to clear events out of the data store. This is to reset the events entirely.
@@ -21,9 +21,11 @@ let template = `
21
21
  <div class="my-3">
22
22
  <v-select id="logselect"
23
23
  :clearable="true" :searchable="true"
24
- :filterable="true" :options="options"
24
+ :filterable="true" :options="logs"
25
25
  v-model="selected">
26
26
  </v-select>
27
+ <input name="scroll" type="checkbox" v-model="scroll" />
28
+ <label for="scroll">Scroll Log Output</label>
27
29
  </div>
28
30
  </div>
29
31
  <div class="fp-scroll-container">
@@ -31,6 +33,7 @@ let template = `
31
33
  <pre><code>{{ text }}</code></pre>
32
34
  </div>
33
35
  </div>
36
+ <div class="alert alert-danger" role="alert" v-if="error">{{ error }}</div>
34
37
  </div>
35
38
  `;
36
39
 
@@ -40,19 +43,10 @@ Vue.component('v-select', VueSelect.VueSelect);
40
43
 
41
44
  Vue.component("logging", {
42
45
  template: template,
43
- data() {return {"selected": "", "logs": _datastore.logs, text: ""}},
46
+ data() {return {"selected": "", "logs": _datastore.logs, text: "", "scroll": true, "error": ""}},
44
47
  mounted() {
45
48
  setInterval(this.update, 1000); // Grab log updates once a second
46
49
  },
47
- computed:{
48
- /**
49
- * Computes the appropriate log files available.
50
- * @return {string[]}
51
- */
52
- options: function () {
53
- return this.logs;
54
- }
55
- },
56
50
  methods: {
57
51
  /**
58
52
  * Updates the log data such that new logs can be displayed.
@@ -65,8 +59,22 @@ Vue.component("logging", {
65
59
  _loader.load("/logdata/" + this.selected, "GET").then(
66
60
  (result) => {
67
61
  _self.text = result[_self.selected];
68
- this.$el.scrollTop = this.$el.scrollHeight
69
- }).catch((result) => {_self.text = "[ERROR] "+ result});
62
+ // Update on next-tick so that the updated content has been drawn already
63
+ _self.$nextTick(() => {
64
+ let panes = _self.$el.getElementsByClassName("fp-scrollable");
65
+ if (panes && _self.scroll)
66
+ {
67
+ panes[0].scrollTop = panes[0].scrollHeight;
68
+ }
69
+ });
70
+ _self.error = "";
71
+ }).catch((result) => {
72
+ if (result === "") {
73
+ _self.error = "[ERROR] Failed to update log content.";
74
+ } else {
75
+ _self.error = "[ERROR] " + result + ".";
76
+ }
77
+ });
70
78
  }
71
79
  }
72
80
  });
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fprime-gds
3
- Version: 3.3.2
3
+ Version: 3.4.0
4
4
  Summary: F Prime Flight Software Ground Data System layer.
5
5
  Home-page: https://github.com/nasa/fprime
6
6
  Author: Michael Starch
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
23
23
  Requires-Python: >=3.7
24
24
  License-File: LICENSE.txt
25
25
  License-File: NOTICE.txt
26
- Requires-Dist: flask <=2.2.3,>=1.1.2
26
+ Requires-Dist: flask >=3.0.0
27
27
  Requires-Dist: flask-compress >=1.11
28
28
  Requires-Dist: pyzmq >=24.0.1
29
29
  Requires-Dist: pexpect >=4.8.0