Merge "Add ability to query falcon configuration dynamically"
diff --git a/aos/containers/BUILD b/aos/containers/BUILD
index 9fdc93f..ee1bf98 100644
--- a/aos/containers/BUILD
+++ b/aos/containers/BUILD
@@ -64,6 +64,30 @@
)
cc_library(
+ name = "error_list",
+ hdrs = [
+ "error_list.h",
+ ],
+ deps = [
+ ":sized_array",
+ "//aos:flatbuffers",
+ ],
+)
+
+cc_test(
+ name = "error_list_test",
+ srcs = [
+ "error_list_test.cc",
+ ],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ ":error_list",
+ "//aos:json_to_flatbuffer_fbs",
+ "//aos/testing:googletest",
+ ],
+)
+
+cc_library(
name = "resizeable_buffer",
hdrs = [
"resizeable_buffer.h",
diff --git a/aos/containers/error_list.h b/aos/containers/error_list.h
new file mode 100644
index 0000000..2fbd39e
--- /dev/null
+++ b/aos/containers/error_list.h
@@ -0,0 +1,130 @@
+#ifndef AOS_CONTAINERS_ERROR_LIST_H_
+#define AOS_CONTAINERS_ERROR_LIST_H_
+
+#include <iostream>
+
+#include "aos/containers/sized_array.h"
+#include "flatbuffers/flatbuffers.h"
+
+namespace aos {
+
+// A de-duplicated sorted array based on SizedArray
+// For keeping a list of errors that a subsystem has thrown
+// to publish them in a Status message.
+// It is designed to use flatbuffer enums, and use the reserved fields MAX and
+// MIN to automatically determine how much capacity it needs to have.
+template <typename T>
+class ErrorList {
+ private:
+ using array = SizedArray<T, static_cast<size_t>(T::MAX) -
+ static_cast<size_t>(T::MIN) + 1>;
+ array array_;
+
+ public:
+ using value_type = typename array::value_type;
+ using size_type = typename array::size_type;
+ using difference_type = typename array::difference_type;
+ using reference = typename array::reference;
+ using const_reference = typename array::const_reference;
+ using pointer = typename array::pointer;
+ using const_pointer = typename array::const_pointer;
+ using iterator = typename array::iterator;
+ using const_iterator = typename array::const_iterator;
+ using reverse_iterator = typename array::reverse_iterator;
+ using const_reverse_iterator = typename array::const_reverse_iterator;
+
+ constexpr ErrorList() = default;
+ ErrorList(const ErrorList &) = default;
+ ErrorList(ErrorList &&) = default;
+ ErrorList(const flatbuffers::Vector<T> &array) : array_() {
+ for (auto it = array.begin(); it < array.end(); it++) {
+ array_.push_back(*it);
+ }
+ std::sort(array_.begin(), array_.end());
+ };
+
+ ErrorList &operator=(const ErrorList &) = default;
+ ErrorList &operator=(ErrorList &&) = default;
+
+ bool operator==(const ErrorList &other) const {
+ if (other.size() != size()) {
+ return false;
+ }
+ for (size_t i = 0; i < size(); ++i) {
+ if (other[i] != (*this)[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ bool operator!=(const ErrorList &other) const { return !(*this == other); }
+
+ reference at(size_t i) { return array_.at(i); }
+ const_reference at(size_t i) const { return array_.at(i); }
+
+ reference operator[](size_t i) { return array_[i]; }
+ const_reference operator[](size_t i) const { return array_[i]; }
+
+ reference front() { return array_.front(); }
+ const_reference front() const { return array_.front(); }
+
+ reference back() { return array_.back(); }
+ const_reference back() const { return array_.back(); }
+
+ T *data() { return array_.data(); }
+ const T *data() const { return array_.data(); }
+
+ iterator begin() { return array_.begin(); }
+ const_iterator begin() const { return array_.begin(); }
+ const_iterator cbegin() const { return array_.cbegin(); }
+
+ iterator end() { return array_.end(); }
+ const_iterator end() const { return array_.end(); }
+ const_iterator cend() const { return array_.cend(); }
+
+ reverse_iterator rbegin() { return array_.rbegin(); }
+ const_reverse_iterator rbegin() const { return array_.rbegin(); }
+ const_reverse_iterator crbegin() const { return array_.crbegin(); }
+
+ reverse_iterator rend() { return array_.rend(); }
+ const_reverse_iterator rend() const { return array_.rend(); }
+ const_reverse_iterator crend() const { return array_.crend(); }
+
+ bool empty() const { return array_.empty(); }
+ bool full() const { return array_.full(); }
+
+ size_t size() const { return array_.size(); }
+ constexpr size_t max_size() const { return array_.max_size(); }
+
+ void Clear(const T t) {
+ iterator index = std::find(array_.begin(), array_.end(), t);
+ if (index != array_.end()) {
+ array_.erase(index);
+ }
+ }
+
+ void Set(const T t) {
+ iterator position = std::lower_bound(array_.begin(), array_.end(), t);
+
+ // if it found something, and that something is the same, just leave it
+ if (position != array_.end() && *position == t) {
+ return;
+ }
+
+ // key doesn't already exist
+ array_.insert(position, t);
+ }
+
+ bool Has(const T t) {
+ return std::binary_search(array_.begin(), array_.end(), t);
+ }
+
+ flatbuffers::Offset<flatbuffers::Vector<T>> ToFlatbuffer(
+ flatbuffers::FlatBufferBuilder *fbb) const {
+ return fbb->CreateVector(array_.data(), array_.size());
+ }
+}; // namespace aos
+
+} // namespace aos
+
+#endif // AOS_CONTAINERS_ERROR_LIST_H_
diff --git a/aos/containers/error_list_test.cc b/aos/containers/error_list_test.cc
new file mode 100644
index 0000000..3ce23c4
--- /dev/null
+++ b/aos/containers/error_list_test.cc
@@ -0,0 +1,115 @@
+#include "aos/containers/error_list.h"
+
+#include "aos/json_to_flatbuffer_generated.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+
+enum class TestEnum : int8_t {
+ FOO = 0,
+ BAR = 1,
+ BAZ = 2,
+ VWEEP = 3,
+ MIN = FOO,
+ MAX = VWEEP
+};
+
+// Tests that setting works and allows no duplicates
+TEST(ErrorListTest, NoDuplicates) {
+ ErrorList<TestEnum> a;
+ EXPECT_EQ(a.size(), 0);
+ a.Set(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.size(), 1);
+ a.Set(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.size(), 1);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 2);
+ a.Set(TestEnum::FOO);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+}
+
+// Tests that clearing works
+TEST(ErrorListTest, Clearing) {
+ ErrorList<TestEnum> a;
+ a.Set(TestEnum::FOO);
+ a.Set(TestEnum::BAZ);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ a.Clear(TestEnum::BAR);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ a.Clear(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 2);
+}
+
+// Tests that checking for a value works
+TEST(ErrorListTest, Has) {
+ ErrorList<TestEnum> a;
+ a.Set(TestEnum::FOO);
+ a.Set(TestEnum::BAZ);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ EXPECT_TRUE(a.Has(TestEnum::FOO));
+ EXPECT_TRUE(a.Has(TestEnum::VWEEP));
+ EXPECT_TRUE(a.Has(TestEnum::BAZ));
+ EXPECT_FALSE(a.Has(TestEnum::BAR));
+}
+
+// Tests serializing and deserializing to/from flatbuffers.
+TEST(ErrorListTest, Flatbuffers) {
+ ErrorList<BaseType> a;
+ a.Set(BaseType::Bool);
+ a.Set(BaseType::Float);
+ a.Set(BaseType::Short);
+ EXPECT_TRUE(a.Has(BaseType::Bool));
+ EXPECT_TRUE(a.Has(BaseType::Short));
+ EXPECT_TRUE(a.Has(BaseType::Float));
+ EXPECT_EQ(a.at(0), BaseType::Bool);
+ EXPECT_EQ(a.at(1), BaseType::Short);
+ EXPECT_EQ(a.at(2), BaseType::Float);
+ EXPECT_EQ(a.size(), 3);
+
+ flatbuffers::FlatBufferBuilder fbb(1024);
+ flatbuffers::Offset<flatbuffers::Vector<BaseType>> vector =
+ a.ToFlatbuffer(&fbb);
+
+ ConfigurationBuilder builder(fbb);
+ builder.add_vector_foo_enum(vector);
+
+ fbb.Finish(builder.Finish());
+ const Configuration *config =
+ flatbuffers::GetRoot<Configuration>(fbb.GetBufferPointer());
+
+ ErrorList<BaseType> b(*config->vector_foo_enum());
+ EXPECT_TRUE(b.Has(BaseType::Bool));
+ EXPECT_TRUE(b.Has(BaseType::Short));
+ EXPECT_TRUE(b.Has(BaseType::Float));
+ EXPECT_EQ(b.at(0), BaseType::Bool);
+ EXPECT_EQ(b.at(1), BaseType::Short);
+ EXPECT_EQ(b.at(2), BaseType::Float);
+ EXPECT_EQ(b.size(), 3);
+}
+
+} // namespace testing
+} // namespace aos
diff --git a/aos/containers/sized_array_test.cc b/aos/containers/sized_array_test.cc
index ae732bc..d055f40 100644
--- a/aos/containers/sized_array_test.cc
+++ b/aos/containers/sized_array_test.cc
@@ -175,5 +175,70 @@
EXPECT_DEATH(a.emplace_back(5), "Aborted at");
}
+// Tests inserting at various positions in the array.
+TEST(SizedArrayTest, Inserting) {
+ SizedArray<int, 5> a;
+ a.insert(a.begin(), 2);
+ EXPECT_EQ(a.at(0), 2);
+ EXPECT_EQ(a.size(), 1);
+
+ a.emplace_back(3);
+ EXPECT_EQ(a.at(0), 2);
+ EXPECT_EQ(a.at(1), 3);
+ EXPECT_EQ(a.size(), 2);
+
+ a.insert(a.begin(), 0);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 2);
+ EXPECT_EQ(a.at(2), 3);
+ EXPECT_EQ(a.size(), 3);
+
+ a.insert(a.begin() + 1, 1);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 1);
+ EXPECT_EQ(a.at(2), 2);
+ EXPECT_EQ(a.at(3), 3);
+ EXPECT_EQ(a.size(), 4);
+
+ a.insert(a.begin() + 1, 0);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 0);
+ EXPECT_EQ(a.at(2), 1);
+ EXPECT_EQ(a.at(3), 2);
+ EXPECT_EQ(a.at(4), 3);
+ EXPECT_EQ(a.size(), 5);
+}
+
+// Tests erasing things from the array
+TEST(SizedArrayTest, Erasing) {
+ SizedArray<int, 5> a;
+ a.push_back(8);
+ a.push_back(9);
+ a.push_back(7);
+ a.push_back(1);
+ a.push_back(5);
+ EXPECT_EQ(a.at(0), 8);
+ EXPECT_EQ(a.at(1), 9);
+ EXPECT_EQ(a.at(2), 7);
+ EXPECT_EQ(a.at(3), 1);
+ EXPECT_EQ(a.at(4), 5);
+ EXPECT_EQ(a.size(), 5);
+
+ a.erase(a.begin() + 1, a.begin() + 3);
+ EXPECT_EQ(a.at(0), 8);
+ EXPECT_EQ(a.at(1), 1);
+ EXPECT_EQ(a.at(2), 5);
+ EXPECT_EQ(a.size(), 3);
+
+ a.erase(a.begin());
+ EXPECT_EQ(a.at(0), 1);
+ EXPECT_EQ(a.at(1), 5);
+ EXPECT_EQ(a.size(), 2);
+
+ a.erase(a.end() - 1);
+ EXPECT_EQ(a.at(0), 1);
+ EXPECT_EQ(a.size(), 1);
+}
+
} // namespace testing
} // namespace aos
diff --git a/aos/util/foxglove_websocket.cc b/aos/util/foxglove_websocket.cc
index 715ba13..77dca20 100644
--- a/aos/util/foxglove_websocket.cc
+++ b/aos/util/foxglove_websocket.cc
@@ -11,6 +11,10 @@
"with a read_method of PIN (see aos/configuration.fbs; PIN is an "
"enum value). Having this enabled will cause foxglove to consume "
"extra shared memory resources.");
+DEFINE_bool(
+ canonical_channel_names, false,
+ "If set, use full channel names; by default, will shorten names to be the "
+ "shortest possible version of the name (e.g., /aos instead of /pi/aos).");
int main(int argc, char *argv[]) {
gflags::SetUsageMessage(
@@ -53,7 +57,10 @@
: aos::FoxgloveWebsocketServer::Serialization::kJson,
FLAGS_fetch_pinned_channels
? aos::FoxgloveWebsocketServer::FetchPinnedChannels::kYes
- : aos::FoxgloveWebsocketServer::FetchPinnedChannels::kNo);
+ : aos::FoxgloveWebsocketServer::FetchPinnedChannels::kNo,
+ FLAGS_canonical_channel_names
+ ? aos::FoxgloveWebsocketServer::CanonicalChannelNames::kCanonical
+ : aos::FoxgloveWebsocketServer::CanonicalChannelNames::kShortened);
event_loop.Run();
}
diff --git a/aos/util/foxglove_websocket_lib.cc b/aos/util/foxglove_websocket_lib.cc
index 1cc3a8a..06c551e 100644
--- a/aos/util/foxglove_websocket_lib.cc
+++ b/aos/util/foxglove_websocket_lib.cc
@@ -1,8 +1,8 @@
#include "aos/util/foxglove_websocket_lib.h"
-#include "aos/util/mcap_logger.h"
-#include "aos/flatbuffer_merge.h"
#include "absl/strings/escaping.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/util/mcap_logger.h"
#include "gflags/gflags.h"
DEFINE_uint32(sorting_buffer_ms, 100,
@@ -17,10 +17,12 @@
namespace aos {
FoxgloveWebsocketServer::FoxgloveWebsocketServer(
aos::EventLoop *event_loop, uint32_t port, Serialization serialization,
- FetchPinnedChannels fetch_pinned_channels)
+ FetchPinnedChannels fetch_pinned_channels,
+ CanonicalChannelNames canonical_channels)
: event_loop_(event_loop),
serialization_(serialization),
fetch_pinned_channels_(fetch_pinned_channels),
+ canonical_channels_(canonical_channels),
server_(port, "aos_foxglove") {
for (const aos::Channel *channel :
*event_loop_->configuration()->channels()) {
@@ -30,18 +32,28 @@
(!is_pinned || fetch_pinned_channels_ == FetchPinnedChannels::kYes)) {
const FlatbufferDetachedBuffer<reflection::Schema> schema =
RecursiveCopyFlatBuffer(channel->schema());
+ const std::string shortest_name =
+ ShortenedChannelName(event_loop_->configuration(), channel,
+ event_loop_->name(), event_loop_->node());
+ std::string name_to_send;
+ switch (canonical_channels_) {
+ case CanonicalChannelNames::kCanonical:
+ name_to_send = channel->name()->string_view();
+ break;
+ case CanonicalChannelNames::kShortened:
+ name_to_send = shortest_name;
+ break;
+ }
const ChannelId id =
(serialization_ == Serialization::kJson)
? server_.addChannel(foxglove::websocket::ChannelWithoutId{
- .topic =
- channel->name()->str() + " " + channel->type()->str(),
+ .topic = name_to_send + " " + channel->type()->str(),
.encoding = "json",
.schemaName = channel->type()->str(),
.schema =
JsonSchemaForFlatbuffer({channel->schema()}).dump()})
: server_.addChannel(foxglove::websocket::ChannelWithoutId{
- .topic =
- channel->name()->str() + " " + channel->type()->str(),
+ .topic = name_to_send + " " + channel->type()->str(),
.encoding = "flatbuffer",
.schemaName = channel->type()->str(),
.schema = absl::Base64Escape(
diff --git a/aos/util/foxglove_websocket_lib.h b/aos/util/foxglove_websocket_lib.h
index 9be2f61..7c326c2 100644
--- a/aos/util/foxglove_websocket_lib.h
+++ b/aos/util/foxglove_websocket_lib.h
@@ -24,9 +24,20 @@
kYes,
kNo,
};
+ // Whether to attempt to shorten channel names.
+ enum class CanonicalChannelNames {
+ // Just use the full, unambiguous, channel names.
+ kCanonical,
+ // Use GetChannelAliases() to determine the shortest possible name for the
+ // channel for the current node, and use that in the MCAP file. This makes
+ // it so that the channels in the resulting file are more likely to match
+ // the channel names that are used in "real" applications.
+ kShortened,
+ };
FoxgloveWebsocketServer(aos::EventLoop *event_loop, uint32_t port,
Serialization serialization,
- FetchPinnedChannels fetch_pinned_channels);
+ FetchPinnedChannels fetch_pinned_channels,
+ CanonicalChannelNames canonical_channels);
~FoxgloveWebsocketServer();
private:
@@ -47,6 +58,7 @@
aos::EventLoop *event_loop_;
const Serialization serialization_;
const FetchPinnedChannels fetch_pinned_channels_;
+ const CanonicalChannelNames canonical_channels_;
foxglove::websocket::Server server_;
// A map of fetchers for every single channel that could be subscribed to.
std::map<ChannelId, FetcherState> fetchers_;
diff --git a/aos/util/mcap_logger.cc b/aos/util/mcap_logger.cc
index 40e55f0..111d784 100644
--- a/aos/util/mcap_logger.cc
+++ b/aos/util/mcap_logger.cc
@@ -84,6 +84,21 @@
return schema;
}
+std::string ShortenedChannelName(const aos::Configuration *config,
+ const aos::Channel *channel,
+ std::string_view application_name,
+ const aos::Node *node) {
+ std::set<std::string> names =
+ configuration::GetChannelAliases(config, channel, application_name, node);
+ std::string_view shortest_name;
+ for (const std::string &name : names) {
+ if (shortest_name.empty() || name.size() < shortest_name.size()) {
+ shortest_name = name;
+ }
+ }
+ return std::string(shortest_name);
+}
+
namespace {
std::string_view CompressionName(McapLogger::Compression compression) {
switch (compression) {
@@ -354,15 +369,9 @@
channel->type()->string_view());
break;
case CanonicalChannelNames::kShortened: {
- std::set<std::string> names = configuration::GetChannelAliases(
- event_loop_->configuration(), channel, event_loop_->name(),
- event_loop_->node());
- std::string_view shortest_name;
- for (const std::string &name : names) {
- if (shortest_name.empty() || name.size() < shortest_name.size()) {
- shortest_name = name;
- }
- }
+ const std::string shortest_name =
+ ShortenedChannelName(event_loop_->configuration(), channel,
+ event_loop_->name(), event_loop_->node());
if (shortest_name != channel->name()->string_view()) {
VLOG(1) << "Shortening " << channel->name()->string_view() << " "
<< channel->type()->string_view() << " to " << shortest_name;
diff --git a/aos/util/mcap_logger.h b/aos/util/mcap_logger.h
index c3fdda1..50e9300 100644
--- a/aos/util/mcap_logger.h
+++ b/aos/util/mcap_logger.h
@@ -24,6 +24,13 @@
const FlatbufferType &type,
JsonSchemaRecursion recursion_level = JsonSchemaRecursion::kTopLevel);
+// Returns the shortest possible alias for the specified channel on the
+// specified node/application.
+std::string ShortenedChannelName(const aos::Configuration *config,
+ const aos::Channel *channel,
+ std::string_view application_name,
+ const aos::Node *node);
+
// Generates an MCAP file, per the specification at
// https://github.com/foxglove/mcap/tree/main/docs/specification
// This currently generates an uncompressed logfile with full message indexing
diff --git a/aos/util/mcap_logger_test.cc b/aos/util/mcap_logger_test.cc
index 8bc3419..c6febf9 100644
--- a/aos/util/mcap_logger_test.cc
+++ b/aos/util/mcap_logger_test.cc
@@ -10,7 +10,8 @@
// will require writing an MCAP reader (or importing an existing one).
// Confirm that the schema for the reflection.Schema table itself hasn't
-// changed. reflection.Schema should be a very stable type, so this should need
+// changed. reflection.Schema should be a very stable type, so this should
+// need
// updating except when we change the JSON schema generation itself.
TEST(JsonSchemaTest, ReflectionSchema) {
std::string schema_json =
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
index ed4b9cd..2bf4b4e 100644
--- a/scouting/deploy/BUILD
+++ b/scouting/deploy/BUILD
@@ -17,6 +17,15 @@
include_runfiles = True,
package_dir = "opt/frc971/scouting_server",
strip_prefix = ".",
+ # The "include_runfiles" attribute creates a runfiles tree as seen from
+ # within the workspace directory. But what we really want is the runfiles
+ # tree as seen from the root of the runfiles tree (i.e. one directory up).
+ # So we work around it by manually adding some symlinks that let us pretend
+ # that we're at the root of the runfiles tree.
+ symlinks = {
+ "opt/frc971/scouting_server/org_frc971": ".",
+ "opt/frc971/scouting_server/bazel_tools": "external/bazel_tools",
+ },
)
pkg_tar(
diff --git a/scouting/deploy/scouting.service b/scouting/deploy/scouting.service
index b22a57d..5aa64b0 100644
--- a/scouting/deploy/scouting.service
+++ b/scouting/deploy/scouting.service
@@ -7,6 +7,7 @@
Group=www-data
Type=simple
WorkingDirectory=/opt/frc971/scouting_server
+Environment=RUNFILES_DIR=/opt/frc971/scouting_server
ExecStart=/opt/frc971/scouting_server/scouting/scouting \
-port 8080 \
-db_config /var/frc971/scouting/db_config.json \
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 208141a..afc589e 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -5,16 +5,16 @@
<title>FRC971 Scouting Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
- href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
- integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
+ integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
- href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.c"
/>
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<my-app></my-app>