Revise device configuration logic and settings
The ALSA device scanner no longer inserts default sample rates when it cannot read them, preventing invalid frequency choices Jack server settings now accept independent input and output devices during construction The latency test utility was updated to handle separate input and output device parameters and display them accordingly The help text for the latency tool documents the new usage with optional output device specification Device lists in the configuration dialog are sorted to prefer devices supporting capture and playback, then USB devices, while refresh preserves existing selections
This commit is contained in:
@@ -64,7 +64,7 @@ public:
|
|||||||
|
|
||||||
AlsaFormatEncodeDecodeTest(this);
|
AlsaFormatEncodeDecodeTest(this);
|
||||||
|
|
||||||
JackServerSettings serverSettings("hw:M2",48000,32,3);
|
JackServerSettings serverSettings("hw:M2","hw:M2",48000,32,3);
|
||||||
|
|
||||||
JackConfiguration jackConfiguration;
|
JackConfiguration jackConfiguration;
|
||||||
if (useJack)
|
if (useJack)
|
||||||
|
|||||||
@@ -41,14 +41,15 @@ namespace pipedal
|
|||||||
void SetAlsaInputDevice(const std::string &d){ alsaInputDevice_ = d; }
|
void SetAlsaInputDevice(const std::string &d){ alsaInputDevice_ = d; }
|
||||||
void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; }
|
void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; }
|
||||||
JackServerSettings();
|
JackServerSettings();
|
||||||
JackServerSettings(
|
JackServerSettings(
|
||||||
const std::string alsaInputDevice,
|
const std::string &alsaInputDevice,
|
||||||
|
const std::string &alsaOutputDevice,
|
||||||
uint64_t sampleRate,
|
uint64_t sampleRate,
|
||||||
uint32_t bufferSize,
|
uint32_t bufferSize,
|
||||||
uint32_t numberOfBuffers)
|
uint32_t numberOfBuffers)
|
||||||
: valid_(true),
|
: valid_(true),
|
||||||
alsaInputDevice_(alsaInputDevice),
|
alsaInputDevice_(alsaInputDevice),
|
||||||
alsaOutputDevice_(alsaInputDevice), // default same
|
alsaOutputDevice_(alsaOutputDevice),
|
||||||
sampleRate_(sampleRate),
|
sampleRate_(sampleRate),
|
||||||
bufferSize_(bufferSize),
|
bufferSize_(bufferSize),
|
||||||
numberOfBuffers_(numberOfBuffers),
|
numberOfBuffers_(numberOfBuffers),
|
||||||
|
|||||||
+25
-16
@@ -57,9 +57,9 @@ void PrintHelp()
|
|||||||
pp << "Copyright (c) 2022 Robin Davies\n";
|
pp << "Copyright (c) 2022 Robin Davies\n";
|
||||||
pp << "\n";
|
pp << "\n";
|
||||||
pp << Indent(0) << "Syntax\n\n";
|
pp << Indent(0) << "Syntax\n\n";
|
||||||
pp << Indent(2) << "pipedal_latency_test [<options>] <device-name>\n\n";
|
pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n";
|
||||||
pp << "where <device-name> is the name of an ALSA device. Typically this should be the name of a hardware "
|
pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. "
|
||||||
"device (a device name starting with 'hw:').\n\n";
|
"If <output-device> is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n";
|
||||||
pp << Indent(0) << "Options\n\n";
|
pp << Indent(0) << "Options\n\n";
|
||||||
pp << Indent(15);
|
pp << Indent(15);
|
||||||
|
|
||||||
@@ -95,7 +95,8 @@ void PrintHelp()
|
|||||||
|
|
||||||
pp << Indent(0) << "Examples\n\n";
|
pp << Indent(0) << "Examples\n\n";
|
||||||
pp << Indent(2) << "pipedal_latency_test --list\n\n";
|
pp << Indent(2) << "pipedal_latency_test --list\n\n";
|
||||||
pp << Indent(2) << "pipedal_latency_test hw:M2\n\n";
|
pp << Indent(2) << "pipedal_latency_test hw:M2\n";
|
||||||
|
pp << Indent(2) << "pipedal_latency_test hw:M2 hw:Device2\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListDevices()
|
void ListDevices()
|
||||||
@@ -138,7 +139,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
AudioDriver *audioDriver = nullptr;
|
AudioDriver *audioDriver = nullptr;
|
||||||
|
|
||||||
const std::string &deviceId;
|
const std::string &inputDeviceId;
|
||||||
|
const std::string &outputDeviceId;
|
||||||
ChannelsT inputChannels;
|
ChannelsT inputChannels;
|
||||||
ChannelsT outputChannels;
|
ChannelsT outputChannels;
|
||||||
uint32_t sampleRate;
|
uint32_t sampleRate;
|
||||||
@@ -147,11 +149,13 @@ private:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
AlsaTester(
|
AlsaTester(
|
||||||
const std::string &deviceId,
|
const std::string &inputDeviceId,
|
||||||
|
const std::string &outputDeviceId,
|
||||||
const ChannelsT &inputChannels,
|
const ChannelsT &inputChannels,
|
||||||
const ChannelsT &outputChannels,
|
const ChannelsT &outputChannels,
|
||||||
uint32_t sampleRate, int bufferSize, int buffers)
|
uint32_t sampleRate, int bufferSize, int buffers)
|
||||||
: deviceId(deviceId),
|
: inputDeviceId(inputDeviceId),
|
||||||
|
outputDeviceId(outputDeviceId),
|
||||||
sampleRate(sampleRate),
|
sampleRate(sampleRate),
|
||||||
inputChannels(inputChannels),
|
inputChannels(inputChannels),
|
||||||
outputChannels(outputChannels),
|
outputChannels(outputChannels),
|
||||||
@@ -186,7 +190,7 @@ public:
|
|||||||
TestResult result;
|
TestResult result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JackServerSettings serverSettings(deviceId, sampleRate, bufferSize, buffers);
|
JackServerSettings serverSettings(inputDeviceId, outputDeviceId, sampleRate, bufferSize, buffers);
|
||||||
|
|
||||||
JackConfiguration jackConfiguration;
|
JackConfiguration jackConfiguration;
|
||||||
jackConfiguration.AlsaInitialize(serverSettings);
|
jackConfiguration.AlsaInitialize(serverSettings);
|
||||||
@@ -403,12 +407,13 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
TestResult RunLatencyTest(
|
TestResult RunLatencyTest(
|
||||||
const std::string deviceId,
|
const std::string inputDeviceId,
|
||||||
|
const std::string outputDeviceId,
|
||||||
const ChannelsT &inputChannels,
|
const ChannelsT &inputChannels,
|
||||||
const ChannelsT &outputChannels,
|
const ChannelsT &outputChannels,
|
||||||
uint32_t sampleRate, int bufferSize, int buffers)
|
uint32_t sampleRate, int bufferSize, int buffers)
|
||||||
{
|
{
|
||||||
AlsaTester tester(deviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers);
|
AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers);
|
||||||
return tester.Test();
|
return tester.Test();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,13 +433,14 @@ static std::string overheadDisplay(float value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RunLatencyTest(
|
void RunLatencyTest(
|
||||||
const std::string &deviceId,
|
const std::string &inputDeviceId,
|
||||||
|
const std::string &outputDeviceId,
|
||||||
const ChannelsT &inputChannels,
|
const ChannelsT &inputChannels,
|
||||||
const ChannelsT &outputChannels,
|
const ChannelsT &outputChannels,
|
||||||
uint32_t sampleRate)
|
uint32_t sampleRate)
|
||||||
{
|
{
|
||||||
PrettyPrinter pp;
|
PrettyPrinter pp;
|
||||||
pp << "Device: " << deviceId << " Rate: " << sampleRate << "\n\n";
|
pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n";
|
||||||
|
|
||||||
const int SIZE_COLUMN_WIDTH = 8;
|
const int SIZE_COLUMN_WIDTH = 8;
|
||||||
const int BUFFERS_COLUMN_WIDTH = 20;
|
const int BUFFERS_COLUMN_WIDTH = 20;
|
||||||
@@ -461,7 +467,7 @@ void RunLatencyTest(
|
|||||||
|
|
||||||
for (auto bufferCount : bufferCounts)
|
for (auto bufferCount : bufferCounts)
|
||||||
{
|
{
|
||||||
auto result = RunLatencyTest(deviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount);
|
auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount);
|
||||||
|
|
||||||
pp.Column(column);
|
pp.Column(column);
|
||||||
column += BUFFERS_COLUMN_WIDTH;
|
column += BUFFERS_COLUMN_WIDTH;
|
||||||
@@ -556,11 +562,14 @@ std:
|
|||||||
{
|
{
|
||||||
ListDevices();
|
ListDevices();
|
||||||
}
|
}
|
||||||
else if (parser.Arguments().size() == 1)
|
else if (parser.Arguments().size() >= 1 && parser.Arguments().size() <= 2)
|
||||||
{
|
{
|
||||||
inputChannels = ParseChannels(strInputChannels);
|
inputChannels = ParseChannels(strInputChannels);
|
||||||
outputChannels = ParseChannels(strInputChannels);
|
outputChannels = ParseChannels(strOutputChannels);
|
||||||
RunLatencyTest(parser.Arguments()[0], inputChannels, outputChannels, sampleRate);
|
|
||||||
|
std::string inDev = parser.Arguments()[0];
|
||||||
|
std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev;
|
||||||
|
RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
+13
-20
@@ -119,21 +119,10 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
|||||||
int dir;
|
int dir;
|
||||||
|
|
||||||
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
|
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
|
||||||
if (err != 0)
|
if (err == 0)
|
||||||
{
|
{
|
||||||
Lv2Log::warning(SS("Failed to get minimum sample rate for device '" << info.name_ << "'. Using default 48000."));
|
int err2 = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir);
|
||||||
info.sampleRates_.push_back(48000);
|
if (err2 == 0)
|
||||||
err = 0; // continue using fallback rate
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int err2 = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir);
|
|
||||||
if (err2 != 0)
|
|
||||||
{
|
|
||||||
Lv2Log::warning(SS("Failed to get maximum sample rate for device '" << info.name_ << "'. Using default 48000."));
|
|
||||||
info.sampleRates_.push_back(48000);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i)
|
for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i)
|
||||||
{
|
{
|
||||||
@@ -142,14 +131,18 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
|||||||
{
|
{
|
||||||
info.sampleRates_.push_back(rate);
|
info.sampleRates_.push_back(rate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (info.sampleRates_.empty())
|
}
|
||||||
{
|
else
|
||||||
Lv2Log::warning(SS("No supported sample rates for device '" << info.name_ << "'. Using default 48000."));
|
{
|
||||||
info.sampleRates_.push_back(48000);
|
Lv2Log::warning(SS("Failed to get maximum sample rate for device '" << info.name_ << "'."));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lv2Log::warning(SS("Failed to get minimum sample rate for device '" << info.name_ << "'."));
|
||||||
|
err = 0; // continue using fallback rate for other parameters
|
||||||
|
}
|
||||||
|
|
||||||
err = snd_pcm_hw_params_get_buffer_size_min(params, &minBufferSize);
|
err = snd_pcm_hw_params_get_buffer_size_min(params, &minBufferSize);
|
||||||
if (err == 0)
|
if (err == 0)
|
||||||
|
|||||||
@@ -47,6 +47,22 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
|
|
||||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||||
|
|
||||||
|
function sortDevices(devices: AlsaDeviceInfo[]): AlsaDeviceInfo[] {
|
||||||
|
function category(d: AlsaDeviceInfo): number {
|
||||||
|
if (d.supportsCapture && d.supportsPlayback) return 0;
|
||||||
|
if (d.longName.toLowerCase().includes("usb")) return 1;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let copy = [...devices];
|
||||||
|
copy.sort((a,b) => {
|
||||||
|
let ca = category(a);
|
||||||
|
let cb = category(b);
|
||||||
|
if (ca !== cb) return ca - cb;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
const MIN_BUFFER_SIZE = 16;
|
const MIN_BUFFER_SIZE = 16;
|
||||||
const MAX_BUFFER_SIZE = 2048;
|
const MAX_BUFFER_SIZE = 2048;
|
||||||
|
|
||||||
@@ -263,7 +279,7 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
.then((devices) => {
|
.then((devices) => {
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
if (this.props.open) {
|
if (this.props.open) {
|
||||||
let settings = this.applyAlsaDevices(this.props.jackServerSettings, devices);
|
let settings = this.applyAlsaDevices(this.state.jackServerSettings, devices);
|
||||||
this.setState({
|
this.setState({
|
||||||
alsaDevices: devices,
|
alsaDevices: devices,
|
||||||
jackServerSettings: settings,
|
jackServerSettings: settings,
|
||||||
@@ -292,21 +308,9 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
|
|
||||||
let inDevice = alsaDevices.find(d => d.id === result.alsaInputDevice && d.supportsCapture);
|
let inDevice = alsaDevices.find(d => d.id === result.alsaInputDevice && d.supportsCapture);
|
||||||
let outDevice = alsaDevices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback);
|
let outDevice = alsaDevices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback);
|
||||||
if (!inDevice) {
|
|
||||||
inDevice = alsaDevices.find(d => d.supportsCapture);
|
|
||||||
if (inDevice) {
|
|
||||||
result.alsaInputDevice = inDevice.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!outDevice) {
|
|
||||||
outDevice = alsaDevices.find(d => d.supportsPlayback);
|
|
||||||
if (outDevice) {
|
|
||||||
result.alsaOutputDevice = outDevice.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sameDevice = useSameDevice !== undefined ? useSameDevice : this.state.useSameDevice;
|
const sameDevice = useSameDevice !== undefined ? useSameDevice : this.state.useSameDevice;
|
||||||
if (sameDevice) {
|
if (sameDevice) {
|
||||||
if (!inDevice || !inDevice.supportsPlayback) {
|
if (!inDevice || !inDevice.supportsPlayback) {
|
||||||
const both = alsaDevices.find(d => d.supportsCapture && d.supportsPlayback);
|
const both = alsaDevices.find(d => d.supportsCapture && d.supportsPlayback);
|
||||||
if (both) {
|
if (both) {
|
||||||
@@ -323,6 +327,18 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
} else {
|
} else {
|
||||||
result.alsaOutputDevice = result.alsaInputDevice;
|
result.alsaOutputDevice = result.alsaInputDevice;
|
||||||
outDevice = inDevice;
|
outDevice = inDevice;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!inDevice) {
|
||||||
|
result.valid = false;
|
||||||
|
result.alsaInputDevice = INVALID_DEVICE_ID;
|
||||||
|
}
|
||||||
|
if (!outDevice) {
|
||||||
|
result.valid = false;
|
||||||
|
result.alsaOutputDevice = INVALID_DEVICE_ID;
|
||||||
|
}
|
||||||
|
if (!inDevice || !outDevice) {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,18 +346,17 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
result.valid = false;
|
result.valid = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.sampleRate === 0) result.sampleRate = 48000;
|
let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
|
||||||
let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
|
if (sampleRates.length !== 0 && sampleRates.indexOf(result.sampleRate) === -1) {
|
||||||
if (sampleRates.length === 0) sampleRates = inDevice.sampleRates;
|
let bestSr = sampleRates[0];
|
||||||
if (sampleRates.length === 0) sampleRates = [result.sampleRate || 48000];
|
let bestErr = 1e36;
|
||||||
let bestSr = sampleRates[0];
|
for (let sr of sampleRates) {
|
||||||
let bestErr = 1e36;
|
let err = (sr - result.sampleRate) * (sr - result.sampleRate);
|
||||||
for (let sr of sampleRates) {
|
if (err < bestErr) { bestErr = err; bestSr = sr; }
|
||||||
let err = (sr - result.sampleRate) * (sr - result.sampleRate);
|
}
|
||||||
if (err < bestErr) { bestErr = err; bestSr = sr; }
|
result.sampleRate = bestSr;
|
||||||
}
|
}
|
||||||
result.sampleRate = bestSr;
|
|
||||||
|
|
||||||
let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers);
|
let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers);
|
||||||
result.bufferSize = bestBuffers.bufferSize;
|
result.bufferSize = bestBuffers.bufferSize;
|
||||||
@@ -507,7 +522,9 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice);
|
const sortedDevices = sortDevices(this.state.alsaDevices ?? []);
|
||||||
|
|
||||||
|
let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice);
|
||||||
let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice);
|
let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice);
|
||||||
if (this.state.useSameDevice) {
|
if (this.state.useSameDevice) {
|
||||||
selectedOutputDevice = selectedInputDevice;
|
selectedOutputDevice = selectedInputDevice;
|
||||||
@@ -520,7 +537,7 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
let sampleRates = selectedInputDevice && selectedOutputDevice ?
|
let sampleRates = selectedInputDevice && selectedOutputDevice ?
|
||||||
intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates)
|
intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates)
|
||||||
: (selectedInputDevice ? selectedInputDevice.sampleRates : (selectedOutputDevice ? selectedOutputDevice.sampleRates : []));
|
: (selectedInputDevice ? selectedInputDevice.sampleRates : (selectedOutputDevice ? selectedOutputDevice.sampleRates : []));
|
||||||
let sampleRateOptions = sampleRates.length === 0 ? [this.state.jackServerSettings.sampleRate || 48000] : sampleRates;
|
let sampleRateOptions = sampleRates.length === 0 && this.state.jackServerSettings.sampleRate ? [this.state.jackServerSettings.sampleRate] : sampleRates;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
<DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
||||||
@@ -541,7 +558,7 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
||||||
style={{ width: 220 }}
|
style={{ width: 220 }}
|
||||||
>
|
>
|
||||||
{this.state.alsaDevices?.filter(d => d.supportsCapture && (!this.state.useSameDevice || d.supportsPlayback)).map(d => (
|
{sortedDevices.filter(d => d.supportsCapture && (!this.state.useSameDevice || d.supportsPlayback)).map(d => (
|
||||||
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
||||||
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -558,7 +575,7 @@ const JackServerSettingsDialog = withStyles(
|
|||||||
disabled={this.state.useSameDevice || !this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
disabled={this.state.useSameDevice || !this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
||||||
style={{ width: 220 }}
|
style={{ width: 220 }}
|
||||||
>
|
>
|
||||||
{this.state.alsaDevices?.filter(d => d.supportsPlayback).map(d => (
|
{sortedDevices.filter(d => d.supportsPlayback).map(d => (
|
||||||
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
||||||
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
Reference in New Issue
Block a user