restructure into "server" and "ui" subdirs
Besides being more clear about what belongs to which, this helps with docker caching. The server and ui parts are only rebuilt when their respective subdirectories change. Extend this a bit further by making the webpack build not depend on the target architecture. And adding cache dirs so parts of the server and ui build process can be reused when layer-wide caching fails.
This commit is contained in:
parent
adcfc5e5f0
commit
dd66c7b0dd
|
@ -1,3 +1,4 @@
|
||||||
node_modules
|
/server/target
|
||||||
target
|
/ui/dist
|
||||||
ui-dist
|
/ui/node_modules
|
||||||
|
/ui/yarn-error.log
|
||||||
|
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
server/target
|
||||||
key: ${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt-get update && sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libncurses-dev libsqlite3-dev pkgconf
|
run: sudo apt-get update && sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libncurses-dev libsqlite3-dev pkgconf
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
toolchain: ${{ matrix.rust }}
|
toolchain: ${{ matrix.rust }}
|
||||||
override: true
|
override: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test ${{ matrix.extra_args }} --all
|
run: cd server && cargo test ${{ matrix.extra_args }} --all
|
||||||
js:
|
js:
|
||||||
name: Build and lint Javascript frontend
|
name: Build and lint Javascript frontend
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -57,6 +57,6 @@ jobs:
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
- run: yarn install
|
- run: cd ui && yarn install
|
||||||
- run: yarn build
|
- run: cd ui && yarn build
|
||||||
- run: yarn lint
|
- run: cd ui && yarn lint
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
*.sublime-workspace
|
/server/target
|
||||||
node_modules
|
/ui/dist
|
||||||
prep.config
|
/ui/node_modules
|
||||||
db/schema.rs
|
/ui/yarn-error.log
|
||||||
target
|
|
||||||
ui-dist
|
|
||||||
yarn-error.log
|
|
||||||
|
|
|
@ -3,39 +3,51 @@
|
||||||
# See documentation here:
|
# See documentation here:
|
||||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md
|
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md
|
||||||
|
|
||||||
# Moonfire NVR development environment, using the build platform.
|
# "dev-common" is the portion of "dev" (see below) which isn't specific to the
|
||||||
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS dev
|
# target arch. It's sufficient for building the non-arch-specific webpack.
|
||||||
|
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS dev-common
|
||||||
LABEL maintainer="slamb@slamb.org"
|
LABEL maintainer="slamb@slamb.org"
|
||||||
ARG TARGETARCH
|
|
||||||
ARG BUILDARCH
|
|
||||||
ARG BUILD_UID=1000
|
ARG BUILD_UID=1000
|
||||||
ARG BUILD_GID=1000
|
ARG BUILD_GID=1000
|
||||||
LABEL maintainer="slamb@slamb.org"
|
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
|
COPY docker/dev-common.bash /
|
||||||
|
RUN /dev-common.bash
|
||||||
|
CMD [ "/bin/bash", "--login" ]
|
||||||
|
|
||||||
|
# "dev" is a full development environment, suitable for shelling into or
|
||||||
|
# using with the VS Code container plugin.
|
||||||
|
FROM --platform=$BUILDPLATFORM dev-common AS dev
|
||||||
|
ARG BUILDARCH
|
||||||
|
ARG TARGETARCH
|
||||||
|
LABEL maintainer="slamb@slamb.org"
|
||||||
COPY docker/dev.bash /
|
COPY docker/dev.bash /
|
||||||
RUN /dev.bash
|
RUN /dev.bash
|
||||||
USER moonfire-nvr:moonfire-nvr
|
USER moonfire-nvr:moonfire-nvr
|
||||||
WORKDIR /var/lib/moonfire-nvr
|
WORKDIR /var/lib/moonfire-nvr
|
||||||
CMD [ "/bin/bash", "--login" ]
|
|
||||||
|
|
||||||
# Build the webpack with node_modules and ui-dist outside the src dir.
|
# Build the UI with node_modules and ui-dist outside the src dir.
|
||||||
FROM dev AS build-webpack
|
FROM --platform=$BUILDPLATFORM dev-common AS build-ui
|
||||||
LABEL maintainer="slamb@slamb.org"
|
LABEL maintainer="slamb@slamb.org"
|
||||||
RUN --mount=type=bind,target=/var/lib/moonfire-nvr/src,readonly \
|
WORKDIR /var/lib/moonfire-nvr/src/ui
|
||||||
ln -s src/package.json src/yarn.lock src/ui-src src/webpack . && \
|
COPY ui /var/lib/moonfire-nvr/src/ui
|
||||||
yarn install && yarn build && rm -rf node_modules
|
RUN --mount=type=cache,target=/var/lib/moonfire-nvr/src/ui/node_modules,sharing=locked,mode=1777 \
|
||||||
|
yarn install && yarn build
|
||||||
|
|
||||||
# Build the Rust components. Note that dev.sh set up an environment variable
|
# Build the Rust components. Note that dev.sh set up an environment variable
|
||||||
# in .buildrc that similarly changes the target dir path.
|
# in .buildrc that similarly changes the target dir path.
|
||||||
FROM dev AS build-server
|
FROM --platform=$BUILDPLATFORM dev AS build-server
|
||||||
LABEL maintainer="slamb@slamb.org"
|
LABEL maintainer="slamb@slamb.org"
|
||||||
RUN --mount=type=bind,target=/var/lib/moonfire-nvr/src,readonly \
|
RUN --mount=type=cache,id=target,target=/var/lib/moonfire-nvr/target,sharing=locked,mode=1777 \
|
||||||
bash -c 'set -o xtrace && source ~/.buildrc && cd src && cargo test && cargo build --release'
|
--mount=type=bind,source=server,target=/var/lib/moonfire-nvr/src/server,readonly \
|
||||||
|
bash -c 'set -o xtrace && \
|
||||||
|
source ~/.buildrc && \
|
||||||
|
cd src/server && \
|
||||||
|
cargo test && \
|
||||||
|
cargo build --release && \
|
||||||
|
sudo install -m 755 ~/moonfire-nvr /usr/local/bin/moonfire-nvr'
|
||||||
|
|
||||||
# Deployment environment, now in the target platform.
|
# Deployment environment, now in the target platform.
|
||||||
FROM --platform=$TARGETPLATFORM ubuntu:20.04 AS deploy
|
FROM --platform=$TARGETPLATFORM ubuntu:20.04 AS deploy
|
||||||
ARG DEPLOY_UID=10000
|
|
||||||
ARG DEPLOY_GID=10000
|
|
||||||
LABEL maintainer="slamb@slamb.org"
|
LABEL maintainer="slamb@slamb.org"
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||||
|
@ -51,21 +63,12 @@ RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||||
vim-nox && \
|
vim-nox && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
groupadd \
|
|
||||||
--gid="${DEPLOY_GID}" \
|
|
||||||
moonfire-nvr && \
|
|
||||||
useradd \
|
|
||||||
--no-log-init \
|
|
||||||
--home-dir=/var/lib/moonfire-nvr \
|
|
||||||
--uid="${DEPLOY_UID}" \
|
|
||||||
--gid=moonfire-nvr \
|
|
||||||
--shell=/bin/bash \
|
|
||||||
--create-home \
|
|
||||||
moonfire-nvr && \
|
|
||||||
ln -s moonfire-nvr /usr/local/bin/nvr
|
ln -s moonfire-nvr /usr/local/bin/nvr
|
||||||
COPY --from=build-server /var/lib/moonfire-nvr/moonfire-nvr /usr/local/bin/moonfire-nvr
|
COPY --from=build-server /usr/local/bin/moonfire-nvr /usr/local/bin/moonfire-nvr
|
||||||
COPY --from=build-webpack /var/lib/moonfire-nvr/ui-dist /usr/local/lib/moonfire-nvr/ui
|
COPY --from=build-ui /var/lib/moonfire-nvr/ui/dist /usr/local/lib/moonfire-nvr/ui
|
||||||
|
|
||||||
USER moonfire-nvr:moonfire-nvr
|
# The install instructions say to use --user in the docker run commandline.
|
||||||
|
# Specify a non-root user just in case someone forgets.
|
||||||
|
USER 10000:10000
|
||||||
WORKDIR /var/lib/moonfire-nvr
|
WORKDIR /var/lib/moonfire-nvr
|
||||||
ENTRYPOINT [ "/usr/local/bin/moonfire-nvr" ]
|
ENTRYPOINT [ "/usr/local/bin/moonfire-nvr" ]
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build the "dev" target. See Dockerfile.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
set -o xtrace
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
packages=()
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
|
||||||
|
# Add yarn repository.
|
||||||
|
apt-get --assume-yes --no-install-recommends install curl gnupg ca-certificates
|
||||||
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||||
|
echo "deb https://dl.yarnpkg.com/debian/ stable main" \
|
||||||
|
>> /etc/apt/sources.list.d/yarn.list
|
||||||
|
|
||||||
|
# Install all packages necessary for building (and some for testing/debugging).
|
||||||
|
packages+=(
|
||||||
|
build-essential
|
||||||
|
pkgconf
|
||||||
|
locales
|
||||||
|
nodejs
|
||||||
|
sudo
|
||||||
|
sqlite3
|
||||||
|
tzdata
|
||||||
|
vim-nox
|
||||||
|
yarn
|
||||||
|
)
|
||||||
|
apt-get update
|
||||||
|
apt-get install --assume-yes --no-install-recommends "${packages[@]}"
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create the user. On the dev environment, allow sudo.
|
||||||
|
groupadd \
|
||||||
|
--gid="${BUILD_GID}" \
|
||||||
|
moonfire-nvr
|
||||||
|
useradd \
|
||||||
|
--no-log-init \
|
||||||
|
--home-dir=/var/lib/moonfire-nvr \
|
||||||
|
--uid="${BUILD_UID}" \
|
||||||
|
--gid=moonfire-nvr \
|
||||||
|
--shell=/bin/bash \
|
||||||
|
--create-home \
|
||||||
|
moonfire-nvr
|
||||||
|
echo 'moonfire-nvr ALL=(ALL) NOPASSWD: ALL' >>/etc/sudoers
|
||||||
|
|
||||||
|
# Install Rust. Note curl was already installed for yarn above.
|
||||||
|
su moonfire-nvr -lc "curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs |
|
||||||
|
sh -s - -y"
|
||||||
|
|
||||||
|
# Put configuration for the Rust build into a new ".buildrc" which is used
|
||||||
|
# both (1) interactively from ~/.bashrc when logging into the dev container
|
||||||
|
# and (2) from a build-server RUN command. In particular, the latter can't
|
||||||
|
# use ~/.bashrc because that script immediately exits when run from a
|
||||||
|
# non-interactive shell.
|
||||||
|
echo 'source $HOME/.buildrc' >> /var/lib/moonfire-nvr/.bashrc
|
||||||
|
cat >> /var/lib/moonfire-nvr/.buildrc <<EOF
|
||||||
|
source \$HOME/.cargo/env
|
||||||
|
|
||||||
|
# Set the target directory to be outside the src bind mount.
|
||||||
|
# https://doc.rust-lang.org/cargo/reference/config.html#buildtarget-dir
|
||||||
|
export CARGO_BUILD_TARGET_DIR=/var/lib/moonfire-nvr/target
|
||||||
|
EOF
|
||||||
|
chown moonfire-nvr:moonfire-nvr /var/lib/moonfire-nvr/.buildrc
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Build the "dev" target. See Dockerfile.
|
||||||
|
|
||||||
set -o errexit
|
set -o errexit
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
@ -64,68 +65,20 @@ fi
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
|
|
||||||
# Add yarn repository.
|
# Install the packages for the target architecture.
|
||||||
apt-get --assume-yes --no-install-recommends install curl gnupg ca-certificates
|
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" \
|
|
||||||
>> /etc/apt/sources.list.d/yarn.list
|
|
||||||
|
|
||||||
# Install all packages necessary for building (and some for testing/debugging).
|
|
||||||
packages+=(
|
packages+=(
|
||||||
build-essential
|
|
||||||
pkgconf
|
|
||||||
ffmpeg"${apt_target_suffix}"
|
ffmpeg"${apt_target_suffix}"
|
||||||
libavcodec-dev"${apt_target_suffix}"
|
libavcodec-dev"${apt_target_suffix}"
|
||||||
libavformat-dev"${apt_target_suffix}"
|
libavformat-dev"${apt_target_suffix}"
|
||||||
libavutil-dev"${apt_target_suffix}"
|
libavutil-dev"${apt_target_suffix}"
|
||||||
libncurses-dev"${apt_target_suffix}"
|
libncurses-dev"${apt_target_suffix}"
|
||||||
libsqlite3-dev"${apt_target_suffix}"
|
libsqlite3-dev"${apt_target_suffix}"
|
||||||
locales
|
|
||||||
nodejs
|
|
||||||
sudo
|
|
||||||
sqlite3
|
|
||||||
tzdata
|
|
||||||
vim-nox
|
|
||||||
yarn
|
|
||||||
)
|
)
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install --assume-yes --no-install-recommends "${packages[@]}"
|
apt-get install --assume-yes --no-install-recommends "${packages[@]}"
|
||||||
apt-get clean
|
apt-get clean
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create the user. On the dev environment, allow sudo.
|
|
||||||
groupadd \
|
|
||||||
--gid="${BUILD_GID}" \
|
|
||||||
moonfire-nvr
|
|
||||||
useradd \
|
|
||||||
--no-log-init \
|
|
||||||
--home-dir=/var/lib/moonfire-nvr \
|
|
||||||
--uid="${BUILD_UID}" \
|
|
||||||
--gid=moonfire-nvr \
|
|
||||||
--shell=/bin/bash \
|
|
||||||
--create-home \
|
|
||||||
moonfire-nvr
|
|
||||||
echo 'moonfire-nvr ALL=(ALL) NOPASSWD: ALL' >>/etc/sudoers
|
|
||||||
|
|
||||||
# Install Rust. Note curl was already installed for yarn above.
|
|
||||||
su moonfire-nvr -lc "curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs |
|
|
||||||
sh -s - -y"
|
|
||||||
|
|
||||||
# Put configuration for the Rust build into a new ".buildrc" which is used
|
|
||||||
# both (1) interactively from ~/.bashrc when logging into the dev container
|
|
||||||
# and (2) from a build-server RUN command. In particular, the latter can't
|
|
||||||
# use ~/.bashrc because that script immediately exits when run from a
|
|
||||||
# non-interactive shell.
|
|
||||||
echo 'source $HOME/.buildrc' >> /var/lib/moonfire-nvr/.bashrc
|
|
||||||
cat >> /var/lib/moonfire-nvr/.buildrc <<EOF
|
|
||||||
source \$HOME/.cargo/env
|
|
||||||
|
|
||||||
# Set the target directory to be outside the src bind mount.
|
|
||||||
# https://doc.rust-lang.org/cargo/reference/config.html#buildtarget-dir
|
|
||||||
export CARGO_BUILD_TARGET_DIR=/var/lib/moonfire-nvr/target
|
|
||||||
EOF
|
|
||||||
chown moonfire-nvr:moonfire-nvr /var/lib/moonfire-nvr/.buildrc
|
|
||||||
|
|
||||||
# Set environment variables for cross-compiling.
|
# Set environment variables for cross-compiling.
|
||||||
# Also set up a symlink that points to the release binary, because the
|
# Also set up a symlink that points to the release binary, because the
|
||||||
# release binary's location varies when cross-compiling, as described here:
|
# release binary's location varies when cross-compiling, as described here:
|
||||||
|
|
|
@ -84,10 +84,6 @@ caveats:
|
||||||
$ docker buildx build --load --platform=arm64/v8 ...
|
$ docker buildx build --load --platform=arm64/v8 ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Major caveat: something appears to be making `docker buildx build` frequently
|
|
||||||
redo builds rather than reusing cached results. The author is very annoyed by
|
|
||||||
this and would welcome help in understanding this problem...
|
|
||||||
|
|
||||||
## Non-Docker setup
|
## Non-Docker setup
|
||||||
|
|
||||||
You may prefer building without Docker on the host. Moonfire NVR should run
|
You may prefer building without Docker on the host. Moonfire NVR should run
|
||||||
|
@ -148,13 +144,15 @@ Once prerequisites are installed, you can build the server and find it in
|
||||||
`target/release/moonfire-nvr`:
|
`target/release/moonfire-nvr`:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ cd server
|
||||||
$ cargo test
|
$ cargo test
|
||||||
$ cargo build --release
|
$ cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
You can build the UI via `yarn` and find it in the `ui-dist` directory:
|
You can build the UI via `yarn` and find it in the `ui/dist` directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ cd ui
|
||||||
$ yarn
|
$ yarn
|
||||||
$ yarn build
|
$ yarn build
|
||||||
```
|
```
|
||||||
|
|
|
@ -20,6 +20,7 @@ this in the webpack documentation.
|
||||||
|
|
||||||
Checkout the branch you want to work on and type
|
Checkout the branch you want to work on and type
|
||||||
|
|
||||||
|
$ cd ui
|
||||||
$ yarn start
|
$ yarn start
|
||||||
|
|
||||||
This will pack and prepare a development setup. By default the development
|
This will pack and prepare a development setup. By default the development
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --mode development --config webpack/dev.config.js --progress",
|
"start": "webpack-dev-server --mode development --config webpack/dev.config.js --progress",
|
||||||
"build": "webpack --mode production --config webpack/prod.config.js",
|
"build": "webpack --mode production --config webpack/prod.config.js",
|
||||||
"lint": "eslint ui-src"
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
|
@ -45,3 +45,4 @@
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -37,11 +37,11 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
nvr: './ui-src/index.js',
|
nvr: './src/index.js',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].[chunkhash].js',
|
filename: '[name].[chunkhash].js',
|
||||||
path: path.resolve('./ui-dist/'),
|
path: path.resolve('./dist/'),
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
@ -60,7 +60,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
exclude: /(node_modules|bower_components)/,
|
exclude: /(node_modules|bower_components)/,
|
||||||
include: [path.resolve('./ui-src')],
|
include: [path.resolve('./src')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.png$/,
|
test: /\.png$/,
|
||||||
|
@ -83,7 +83,7 @@ module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.IgnorePlugin(/\.\/locale$/),
|
new webpack.IgnorePlugin(/\.\/locale$/),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './ui-src/index.html',
|
template: './src/index.html',
|
||||||
}),
|
}),
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
/node_modules\/moment\/moment\.js$/,
|
/node_modules\/moment\/moment\.js$/,
|
||||||
|
@ -94,7 +94,7 @@ module.exports = {
|
||||||
'./builds/moment-timezone-with-data-2012-2022.min.js'
|
'./builds/moment-timezone-with-data-2012-2022.min.js'
|
||||||
),
|
),
|
||||||
new FaviconsWebpackPlugin({
|
new FaviconsWebpackPlugin({
|
||||||
logo: './ui-src/favicon.svg',
|
logo: './src/favicon.svg',
|
||||||
mode: 'webapp',
|
mode: 'webapp',
|
||||||
devMode: 'light',
|
devMode: 'light',
|
||||||
prefix: 'favicons-[hash]/',
|
prefix: 'favicons-[hash]/',
|
Loading…
Reference in New Issue