chore: add web/src/common
This commit is contained in:
parent
cc72e736f7
commit
bb13650913
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
|
@ -0,0 +1,102 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const paths = require('./paths');
|
||||||
|
|
||||||
|
// Make sure that including paths.js after env.js will read .env variables.
|
||||||
|
delete require.cache[require.resolve('./paths')];
|
||||||
|
|
||||||
|
const NODE_ENV = process.env.NODE_ENV;
|
||||||
|
if (!NODE_ENV) {
|
||||||
|
throw new Error('The NODE_ENV environment variable is required but was not specified.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||||
|
const dotenvFiles = [
|
||||||
|
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||||
|
// Don't include `.env.local` for `test` environment
|
||||||
|
// since normally you expect tests to produce the same
|
||||||
|
// results for everyone
|
||||||
|
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
|
||||||
|
`${paths.dotenv}.${NODE_ENV}`,
|
||||||
|
paths.dotenv,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Load environment variables from .env* files. Suppress warnings using silent
|
||||||
|
// if this file is missing. dotenv will never modify any environment variables
|
||||||
|
// that have already been set. Variable expansion is supported in .env files.
|
||||||
|
// https://github.com/motdotla/dotenv
|
||||||
|
// https://github.com/motdotla/dotenv-expand
|
||||||
|
dotenvFiles.forEach(dotenvFile => {
|
||||||
|
if (fs.existsSync(dotenvFile)) {
|
||||||
|
require('dotenv-expand')(
|
||||||
|
require('dotenv').config({
|
||||||
|
path: dotenvFile,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We support resolving modules according to `NODE_PATH`.
|
||||||
|
// This lets you use absolute paths in imports inside large monorepos:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253.
|
||||||
|
// It works similar to `NODE_PATH` in Node itself:
|
||||||
|
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||||
|
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||||
|
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||||
|
// We also resolve them to make sure all tools using them work consistently.
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
|
.split(path.delimiter)
|
||||||
|
.filter(folder => folder && !path.isAbsolute(folder))
|
||||||
|
.map(folder => path.resolve(appDirectory, folder))
|
||||||
|
.join(path.delimiter);
|
||||||
|
|
||||||
|
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||||
|
// injected into the application via DefinePlugin in webpack configuration.
|
||||||
|
const REACT_APP = /^REACT_APP_/i;
|
||||||
|
|
||||||
|
function getClientEnvironment(publicUrl) {
|
||||||
|
const raw = Object.keys(process.env)
|
||||||
|
.filter(key => REACT_APP.test(key))
|
||||||
|
.reduce(
|
||||||
|
(env, key) => {
|
||||||
|
env[key] = process.env[key];
|
||||||
|
return env;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Useful for determining whether we’re running in production mode.
|
||||||
|
// Most importantly, it switches React into the correct mode.
|
||||||
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
|
// Useful for resolving the correct path to static assets in `public`.
|
||||||
|
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||||
|
// This should only be used as an escape hatch. Normally you would put
|
||||||
|
// images into the `src` and `import` them in code to get their paths.
|
||||||
|
PUBLIC_URL: publicUrl,
|
||||||
|
// We support configuring the sockjs pathname during development.
|
||||||
|
// These settings let a developer run multiple simultaneous projects.
|
||||||
|
// They are used as the connection `hostname`, `pathname` and `port`
|
||||||
|
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
|
||||||
|
// and `sockPort` options in webpack-dev-server.
|
||||||
|
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
|
||||||
|
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
|
||||||
|
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
|
||||||
|
// Whether or not react-refresh is enabled.
|
||||||
|
// It is defined here so it is available in the webpackHotDevClient.
|
||||||
|
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Stringify all values so we can feed into webpack DefinePlugin
|
||||||
|
const stringified = {
|
||||||
|
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||||
|
env[key] = JSON.stringify(raw[key]);
|
||||||
|
return env;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { raw, stringified };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getClientEnvironment;
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const paths = require('./paths');
|
||||||
|
|
||||||
|
// Ensure the certificate and key provided are valid and if not
|
||||||
|
// throw an easy to debug error
|
||||||
|
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
|
||||||
|
let encrypted;
|
||||||
|
try {
|
||||||
|
// publicEncrypt will throw an error with an invalid cert
|
||||||
|
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// privateDecrypt will throw an error with an invalid key
|
||||||
|
crypto.privateDecrypt(key, encrypted);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file and throw an error if it doesn't exist
|
||||||
|
function readEnvFile(file, type) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(
|
||||||
|
`You specified ${chalk.cyan(type)} in your env, but the file "${chalk.yellow(
|
||||||
|
file,
|
||||||
|
)}" can't be found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fs.readFileSync(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the https config
|
||||||
|
// Return cert files if provided in env, otherwise just true or false
|
||||||
|
function getHttpsConfig() {
|
||||||
|
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
|
||||||
|
const isHttps = HTTPS === 'true';
|
||||||
|
|
||||||
|
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
|
||||||
|
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
|
||||||
|
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
|
||||||
|
const config = {
|
||||||
|
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
|
||||||
|
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
|
||||||
|
};
|
||||||
|
|
||||||
|
validateKeyAndCerts({ ...config, keyFile, crtFile });
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
return isHttps;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getHttpsConfig;
|
|
@ -0,0 +1,29 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const babelJest = require('babel-jest').default;
|
||||||
|
|
||||||
|
const hasJsxRuntime = (() => {
|
||||||
|
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require.resolve('react/jsx-runtime');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
module.exports = babelJest.createTransformer({
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-preset-react-app'),
|
||||||
|
{
|
||||||
|
runtime: hasJsxRuntime ? 'automatic' : 'classic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning style imports into empty objects.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process() {
|
||||||
|
return 'module.exports = {};';
|
||||||
|
},
|
||||||
|
getCacheKey() {
|
||||||
|
// The output is always the same.
|
||||||
|
return 'cssTransform';
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const camelcase = require('camelcase');
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning file imports into filenames.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process(src, filename) {
|
||||||
|
const assetFilename = JSON.stringify(path.basename(filename));
|
||||||
|
|
||||||
|
if (filename.match(/\.svg$/)) {
|
||||||
|
// Based on how SVGR generates a component name:
|
||||||
|
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
||||||
|
const pascalCaseFilename = camelcase(path.parse(filename).name, {
|
||||||
|
pascalCase: true,
|
||||||
|
});
|
||||||
|
const componentName = `Svg${pascalCaseFilename}`;
|
||||||
|
return `const React = require('react');
|
||||||
|
module.exports = {
|
||||||
|
__esModule: true,
|
||||||
|
default: ${assetFilename},
|
||||||
|
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
||||||
|
return {
|
||||||
|
$$typeof: Symbol.for('react.element'),
|
||||||
|
type: 'svg',
|
||||||
|
ref: ref,
|
||||||
|
key: null,
|
||||||
|
props: Object.assign({}, props, {
|
||||||
|
children: ${assetFilename}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `module.exports = ${assetFilename};`;
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,134 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const resolve = require('resolve');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get additional module paths based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
function getAdditionalModulePaths(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
|
||||||
|
// the default behavior.
|
||||||
|
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the user set the `baseUrl` to `appSrc`.
|
||||||
|
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
|
||||||
|
return [paths.appSrc];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is equal to the root directory we ignore it here.
|
||||||
|
// We don't want to allow importing from the root directly as source files are
|
||||||
|
// not transpiled outside of `src`. We do allow importing them with the
|
||||||
|
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
|
||||||
|
// an alias.
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, throw an error.
|
||||||
|
throw new Error(
|
||||||
|
chalk.red.bold(
|
||||||
|
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
|
||||||
|
' Create React App does not support other values at this time.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get webpack aliases based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {*} options
|
||||||
|
*/
|
||||||
|
function getWebpackAliases(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return {
|
||||||
|
src: paths.appSrc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get jest aliases based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {*} options
|
||||||
|
*/
|
||||||
|
function getJestAliases(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return {
|
||||||
|
'^src/(.*)$': '<rootDir>/src/$1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModules() {
|
||||||
|
// Check if TypeScript is setup
|
||||||
|
const hasTsConfig = fs.existsSync(paths.appTsConfig);
|
||||||
|
const hasJsConfig = fs.existsSync(paths.appJsConfig);
|
||||||
|
|
||||||
|
if (hasTsConfig && hasJsConfig) {
|
||||||
|
throw new Error(
|
||||||
|
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
|
||||||
|
// If there's a tsconfig.json we assume it's a
|
||||||
|
// TypeScript project and set up the config
|
||||||
|
// based on tsconfig.json
|
||||||
|
if (hasTsConfig) {
|
||||||
|
const ts = require(resolve.sync('typescript', {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}));
|
||||||
|
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
|
||||||
|
// Otherwise we'll check if there is jsconfig.json
|
||||||
|
// for non TS projects.
|
||||||
|
} else if (hasJsConfig) {
|
||||||
|
config = require(paths.appJsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
config = config || {};
|
||||||
|
const options = config.compilerOptions || {};
|
||||||
|
|
||||||
|
const additionalModulePaths = getAdditionalModulePaths(options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
additionalModulePaths: additionalModulePaths,
|
||||||
|
webpackAliases: getWebpackAliases(options),
|
||||||
|
jestAliases: getJestAliases(options),
|
||||||
|
hasTsConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getModules();
|
|
@ -0,0 +1,77 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
|
||||||
|
|
||||||
|
// Make sure any symlinks in the project folder are resolved:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/637
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||||
|
|
||||||
|
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||||
|
// "public path" at which the app is served.
|
||||||
|
// webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||||
|
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||||
|
// We can't use a relative path in HTML because we don't want to load something
|
||||||
|
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||||
|
const publicUrlOrPath = getPublicUrlOrPath(
|
||||||
|
process.env.NODE_ENV === 'development',
|
||||||
|
require(resolveApp('package.json')).homepage,
|
||||||
|
process.env.PUBLIC_URL,
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildPath = process.env.BUILD_PATH || 'build';
|
||||||
|
|
||||||
|
const moduleFileExtensions = [
|
||||||
|
'web.mjs',
|
||||||
|
'mjs',
|
||||||
|
'web.js',
|
||||||
|
'js',
|
||||||
|
'web.ts',
|
||||||
|
'ts',
|
||||||
|
'web.tsx',
|
||||||
|
'tsx',
|
||||||
|
'json',
|
||||||
|
'web.jsx',
|
||||||
|
'jsx',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Resolve file paths in the same order as webpack
|
||||||
|
const resolveModule = (resolveFn, filePath) => {
|
||||||
|
const extension = moduleFileExtensions.find(extension =>
|
||||||
|
fs.existsSync(resolveFn(`${filePath}.${extension}`)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extension) {
|
||||||
|
return resolveFn(`${filePath}.${extension}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveFn(`${filePath}.js`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// config after eject: we're in ./config/
|
||||||
|
module.exports = {
|
||||||
|
dotenv: resolveApp('.env'),
|
||||||
|
appPath: resolveApp('.'),
|
||||||
|
appBuild: resolveApp(buildPath),
|
||||||
|
appDist: resolveApp('dist'),
|
||||||
|
appPublic: resolveApp('public'),
|
||||||
|
appHtml: resolveApp('public/index.html'),
|
||||||
|
appIndexJs: resolveModule(resolveApp, 'src/index'),
|
||||||
|
appOutputJs: resolveModule(resolveApp, 'src/output'),
|
||||||
|
appPackageJson: resolveApp('package.json'),
|
||||||
|
appSrc: resolveApp('src'),
|
||||||
|
appTsConfig: resolveApp('tsconfig.json'),
|
||||||
|
appJsConfig: resolveApp('jsconfig.json'),
|
||||||
|
yarnLockFile: resolveApp('yarn.lock'),
|
||||||
|
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
|
||||||
|
proxySetup: resolveApp('src/setupProxy.js'),
|
||||||
|
appNodeModules: resolveApp('node_modules'),
|
||||||
|
appWebpackCache: resolveApp('node_modules/.cache'),
|
||||||
|
appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
|
||||||
|
swSrc: resolveModule(resolveApp, 'src/service-worker'),
|
||||||
|
publicUrlOrPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.moduleFileExtensions = moduleFileExtensions;
|
|
@ -0,0 +1,769 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const resolve = require('resolve');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||||
|
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||||
|
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
||||||
|
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||||
|
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||||
|
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||||
|
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
|
||||||
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const modules = require('./modules');
|
||||||
|
const getClientEnvironment = require('./env');
|
||||||
|
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||||
|
const ForkTsCheckerWebpackPlugin =
|
||||||
|
process.env.TSC_COMPILE_ON_ERROR === 'true'
|
||||||
|
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
|
||||||
|
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
|
||||||
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||||
|
|
||||||
|
const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');
|
||||||
|
|
||||||
|
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||||
|
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||||
|
|
||||||
|
const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime');
|
||||||
|
const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
|
||||||
|
'@pmmmwh/react-refresh-webpack-plugin',
|
||||||
|
);
|
||||||
|
const babelRuntimeEntry = require.resolve('babel-preset-react-app');
|
||||||
|
const babelRuntimeEntryHelpers = require.resolve(
|
||||||
|
'@babel/runtime/helpers/esm/assertThisInitialized',
|
||||||
|
{ paths: [babelRuntimeEntry] },
|
||||||
|
);
|
||||||
|
const babelRuntimeRegenerator = require.resolve('@babel/runtime/regenerator', {
|
||||||
|
paths: [babelRuntimeEntry],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||||
|
// makes for a smoother build process.
|
||||||
|
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||||
|
|
||||||
|
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
|
||||||
|
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
|
||||||
|
|
||||||
|
const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10000');
|
||||||
|
|
||||||
|
// Check if TypeScript is setup
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
|
||||||
|
// Check if Tailwind config exists
|
||||||
|
const useTailwind = fs.existsSync(path.join(paths.appPath, 'tailwind.config.js'));
|
||||||
|
|
||||||
|
// Get the path to the uncompiled service worker (if it exists).
|
||||||
|
const swSrc = paths.swSrc;
|
||||||
|
|
||||||
|
// style files regexes
|
||||||
|
const cssRegex = /\.css$/;
|
||||||
|
const cssModuleRegex = /\.module\.css$/;
|
||||||
|
const sassRegex = /\.(scss|sass)$/;
|
||||||
|
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||||
|
const lessRegex = /\.less$/;
|
||||||
|
const lessModuleRegex = /\.module\.less$/;
|
||||||
|
|
||||||
|
const hasJsxRuntime = (() => {
|
||||||
|
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require.resolve('react/jsx-runtime');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// This is the production and development configuration.
|
||||||
|
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||||
|
module.exports = function(webpackEnv) {
|
||||||
|
const isEnvDevelopment = webpackEnv === 'development';
|
||||||
|
const isEnvProduction = webpackEnv === 'production' || webpackEnv === 'package';
|
||||||
|
const isEnvPackage = webpackEnv === 'package';
|
||||||
|
|
||||||
|
// Variable used for enabling profiling in Production
|
||||||
|
// passed into alias object. Uses a flag if passed into the build command
|
||||||
|
const isEnvProductionProfile = isEnvProduction && process.argv.includes('--profile');
|
||||||
|
|
||||||
|
// We will provide `paths.publicUrlOrPath` to our app
|
||||||
|
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||||
|
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||||
|
// Get environment variables to inject into our app.
|
||||||
|
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||||
|
|
||||||
|
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
|
||||||
|
|
||||||
|
// common function to get style loaders
|
||||||
|
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||||
|
const loaders = [
|
||||||
|
isEnvDevelopment && require.resolve('style-loader'),
|
||||||
|
isEnvProduction && {
|
||||||
|
loader: MiniCssExtractPlugin.loader,
|
||||||
|
// css is located in `static/css`, use '../../' to locate index.html folder
|
||||||
|
// in production `paths.publicUrlOrPath` can be a relative path
|
||||||
|
options: paths.publicUrlOrPath.startsWith('.') ? { publicPath: '../../' } : {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve('css-loader'),
|
||||||
|
options: cssOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Options for PostCSS as we reference these options twice
|
||||||
|
// Adds vendor prefixing based on your specified browser support in
|
||||||
|
// package.json
|
||||||
|
loader: require.resolve('postcss-loader'),
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
// Necessary for external CSS imports to work
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2677
|
||||||
|
ident: 'postcss',
|
||||||
|
config: false,
|
||||||
|
plugins: !useTailwind
|
||||||
|
? [
|
||||||
|
'postcss-flexbugs-fixes',
|
||||||
|
[
|
||||||
|
'postcss-preset-env',
|
||||||
|
{
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: 'no-2009',
|
||||||
|
},
|
||||||
|
stage: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Adds PostCSS Normalize as the reset css with default options,
|
||||||
|
// so that it honors browserslist config in package.json
|
||||||
|
// which in turn let's users customize the target behavior as per their needs.
|
||||||
|
'postcss-normalize',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'tailwindcss',
|
||||||
|
'postcss-flexbugs-fixes',
|
||||||
|
[
|
||||||
|
'postcss-preset-env',
|
||||||
|
{
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: 'no-2009',
|
||||||
|
},
|
||||||
|
stage: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean);
|
||||||
|
if (preProcessor) {
|
||||||
|
const preProcessorOptions = {
|
||||||
|
sourceMap: true,
|
||||||
|
};
|
||||||
|
if (preProcessor === 'less-loader') {
|
||||||
|
preProcessorOptions.lessOptions = {
|
||||||
|
javascriptEnabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
loaders.push(
|
||||||
|
{
|
||||||
|
loader: require.resolve('resolve-url-loader'),
|
||||||
|
options: {
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
root: paths.appSrc,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve(preProcessor),
|
||||||
|
options: preProcessorOptions,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return loaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
target: ['browserslist'],
|
||||||
|
// Webpack noise constrained to errors and warnings
|
||||||
|
stats: 'errors-warnings',
|
||||||
|
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
|
||||||
|
// Stop compilation early in production
|
||||||
|
bail: isEnvProduction,
|
||||||
|
devtool: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
? 'source-map'
|
||||||
|
: false
|
||||||
|
: isEnvDevelopment && 'cheap-module-source-map',
|
||||||
|
// These are the "entry points" to our application.
|
||||||
|
// This means they will be the "root" imports that are included in JS bundle.
|
||||||
|
entry: isEnvPackage ? paths.appOutputJs : paths.appIndexJs,
|
||||||
|
output: isEnvPackage
|
||||||
|
? {
|
||||||
|
filename: 'common.min.js',
|
||||||
|
path: paths.appDist,
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
library: 'common',
|
||||||
|
globalObject: 'this',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// The build folder.
|
||||||
|
path: paths.appBuild,
|
||||||
|
// Add /* filename */ comments to generated require()s in the output.
|
||||||
|
pathinfo: isEnvDevelopment,
|
||||||
|
// There will be one main bundle, and one file per asynchronous chunk.
|
||||||
|
// In development, it does not produce real files.
|
||||||
|
filename: isEnvProduction
|
||||||
|
? 'static/js/[name].[contenthash:8].js'
|
||||||
|
: isEnvDevelopment && 'static/js/bundle.js',
|
||||||
|
// There are also additional JS chunk files if you use code splitting.
|
||||||
|
chunkFilename: isEnvProduction
|
||||||
|
? 'static/js/[name].[contenthash:8].chunk.js'
|
||||||
|
: isEnvDevelopment && 'static/js/[name].chunk.js',
|
||||||
|
assetModuleFilename: 'static/media/[name].[hash][ext]',
|
||||||
|
// webpack uses `publicPath` to determine where the app is being served from.
|
||||||
|
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||||
|
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||||
|
publicPath: paths.publicUrlOrPath,
|
||||||
|
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||||
|
devtoolModuleFilenameTemplate: isEnvProduction
|
||||||
|
? info => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/')
|
||||||
|
: isEnvDevelopment &&
|
||||||
|
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
type: 'filesystem',
|
||||||
|
version: createEnvironmentHash(env.raw),
|
||||||
|
cacheDirectory: paths.appWebpackCache,
|
||||||
|
store: 'pack',
|
||||||
|
buildDependencies: {
|
||||||
|
defaultWebpack: ['webpack/lib/'],
|
||||||
|
config: [__filename],
|
||||||
|
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f => fs.existsSync(f)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
infrastructureLogging: {
|
||||||
|
level: 'none',
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isEnvProduction,
|
||||||
|
minimizer: [
|
||||||
|
// This is only used in production mode
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
parse: {
|
||||||
|
// We want terser to parse ecma 8 code. However, we don't want it
|
||||||
|
// to apply any minification steps that turns valid ecma 5 code
|
||||||
|
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||||
|
// sections only apply transformations that are ecma 5 safe
|
||||||
|
// https://github.com/facebook/create-react-app/pull/4234
|
||||||
|
ecma: 8,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
ecma: 5,
|
||||||
|
warnings: false,
|
||||||
|
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2376
|
||||||
|
// Pending further investigation:
|
||||||
|
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||||
|
comparisons: false,
|
||||||
|
// Disabled because of an issue with Terser breaking valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5250
|
||||||
|
// Pending further investigation:
|
||||||
|
// https://github.com/terser-js/terser/issues/120
|
||||||
|
inline: 2,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
// Added for profiling in devtools
|
||||||
|
keep_classnames: isEnvProductionProfile,
|
||||||
|
keep_fnames: isEnvProductionProfile,
|
||||||
|
output: {
|
||||||
|
ecma: 5,
|
||||||
|
comments: false,
|
||||||
|
// Turned on because emoji and regex is not minified properly using default
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2488
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// This is only used in production mode
|
||||||
|
new CssMinimizerPlugin(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
// This allows you to set a fallback for where webpack should look for modules.
|
||||||
|
// We placed these paths second because we want `node_modules` to "win"
|
||||||
|
// if there are any conflicts. This matches Node resolution mechanism.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253
|
||||||
|
modules: ['node_modules', paths.appNodeModules].concat(modules.additionalModulePaths || []),
|
||||||
|
// These are the reasonable defaults supported by the Node ecosystem.
|
||||||
|
// We also include JSX as a common component filename extension to support
|
||||||
|
// some tools, although we do not recommend using it, see:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/290
|
||||||
|
// `web` extension prefixes have been added for better support
|
||||||
|
// for React Native Web.
|
||||||
|
extensions: paths.moduleFileExtensions
|
||||||
|
.map(ext => `.${ext}`)
|
||||||
|
.filter(ext => useTypeScript || !ext.includes('ts')),
|
||||||
|
alias: {
|
||||||
|
// Support React Native Web
|
||||||
|
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||||
|
'react-native': 'react-native-web',
|
||||||
|
// Allows for better profiling with ReactDevTools
|
||||||
|
...(isEnvProductionProfile && {
|
||||||
|
'react-dom$': 'react-dom/profiling',
|
||||||
|
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||||
|
}),
|
||||||
|
...(modules.webpackAliases || {}),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||||
|
// This often causes confusion because we only process files within src/ with babel.
|
||||||
|
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||||
|
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||||
|
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||||
|
new ModuleScopePlugin(paths.appSrc, [
|
||||||
|
paths.appPackageJson,
|
||||||
|
reactRefreshRuntimeEntry,
|
||||||
|
reactRefreshWebpackPluginRuntimeEntry,
|
||||||
|
babelRuntimeEntry,
|
||||||
|
babelRuntimeEntryHelpers,
|
||||||
|
babelRuntimeRegenerator,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
strictExportPresence: true,
|
||||||
|
rules: [
|
||||||
|
// Handle node_modules packages that contain sourcemaps
|
||||||
|
shouldUseSourceMap && {
|
||||||
|
enforce: 'pre',
|
||||||
|
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||||
|
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
|
||||||
|
loader: require.resolve('source-map-loader'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// "oneOf" will traverse all following loaders until one will
|
||||||
|
// match the requirements. When no loader matches it will fall
|
||||||
|
// back to the "file" loader at the end of the loader list.
|
||||||
|
oneOf: [
|
||||||
|
// TODO: Merge this config once `image/avif` is in the mime-db
|
||||||
|
// https://github.com/jshttp/mime-db
|
||||||
|
{
|
||||||
|
test: [/\.avif$/],
|
||||||
|
type: 'asset',
|
||||||
|
mimetype: 'image/avif',
|
||||||
|
parser: {
|
||||||
|
dataUrlCondition: {
|
||||||
|
maxSize: imageInlineSizeLimit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// "url" loader works like "file" loader except that it embeds assets
|
||||||
|
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||||
|
// A missing `test` is equivalent to a match.
|
||||||
|
{
|
||||||
|
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||||
|
type: 'asset',
|
||||||
|
parser: {
|
||||||
|
dataUrlCondition: {
|
||||||
|
maxSize: imageInlineSizeLimit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: require.resolve('@svgr/webpack'),
|
||||||
|
options: {
|
||||||
|
prettier: false,
|
||||||
|
svgo: false,
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
titleProp: true,
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve('file-loader'),
|
||||||
|
options: {
|
||||||
|
name: 'static/media/[name].[hash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
issuer: {
|
||||||
|
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Process application JS with Babel.
|
||||||
|
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||||
|
include: paths.appSrc,
|
||||||
|
loader: require.resolve('babel-loader'),
|
||||||
|
options: {
|
||||||
|
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-preset-react-app'),
|
||||||
|
{
|
||||||
|
runtime: hasJsxRuntime ? 'automatic' : 'classic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
isEnvDevelopment &&
|
||||||
|
shouldUseReactRefresh &&
|
||||||
|
require.resolve('react-refresh/babel'),
|
||||||
|
].filter(Boolean),
|
||||||
|
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||||
|
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||||
|
// directory for faster rebuilds.
|
||||||
|
cacheDirectory: true,
|
||||||
|
// See #6846 for context on why cacheCompression is disabled
|
||||||
|
cacheCompression: false,
|
||||||
|
compact: isEnvProduction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Process any JS outside of the app with Babel.
|
||||||
|
// Unlike the application JS, we only compile the standard ES features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs)$/,
|
||||||
|
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||||
|
loader: require.resolve('babel-loader'),
|
||||||
|
options: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [
|
||||||
|
[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }],
|
||||||
|
],
|
||||||
|
cacheDirectory: true,
|
||||||
|
// See #6846 for context on why cacheCompression is disabled
|
||||||
|
cacheCompression: false,
|
||||||
|
|
||||||
|
// Babel sourcemaps are needed for debugging into node_modules
|
||||||
|
// code. Without the options below, debuggers like VSCode
|
||||||
|
// show incorrect code and set breakpoints on the wrong lines.
|
||||||
|
sourceMaps: shouldUseSourceMap,
|
||||||
|
inputSourceMap: shouldUseSourceMap,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// "postcss" loader applies autoprefixer to our CSS.
|
||||||
|
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||||
|
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||||
|
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||||
|
// to a file, but in development "style" loader enables hot editing
|
||||||
|
// of CSS.
|
||||||
|
// By default we support CSS Modules with the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssRegex,
|
||||||
|
exclude: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
mode: 'icss',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||||
|
// using the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
mode: 'local',
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||||
|
// By default we support SASS Modules with the
|
||||||
|
// extensions .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassRegex,
|
||||||
|
exclude: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
mode: 'icss',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules, but using SASS
|
||||||
|
// using the extension .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
mode: 'local',
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: lessRegex,
|
||||||
|
exclude: lessModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
localIdentName: '[local]_[hash:base64:8]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'less-loader',
|
||||||
|
),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules, but using SASS
|
||||||
|
// using the extension .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: lessModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
localIdentName: '[local]_[hash:base64:8]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'less-loader',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||||
|
// When you `import` an asset, you get its (virtual) filename.
|
||||||
|
// In production, they would get copied to the `build` folder.
|
||||||
|
// This loader doesn't use a "test" so it will catch all modules
|
||||||
|
// that fall through the other loaders.
|
||||||
|
{
|
||||||
|
// Exclude `js` files to keep "css" loader working as it injects
|
||||||
|
// its runtime that would otherwise be processed through "file" loader.
|
||||||
|
// Also exclude `html` and `json` extensions so they get processed
|
||||||
|
// by webpacks internal loaders.
|
||||||
|
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// ** STOP ** Are you adding a new loader?
|
||||||
|
// Make sure to add the new loader(s) before the "file" loader.
|
||||||
|
],
|
||||||
|
},
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Generates an `index.html` file with the <script> injected.
|
||||||
|
new HtmlWebpackPlugin(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
inject: true,
|
||||||
|
template: paths.appHtml,
|
||||||
|
},
|
||||||
|
isEnvProduction
|
||||||
|
? {
|
||||||
|
minify: {
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyURLs: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Inlines the webpack runtime script. This script is too small to warrant
|
||||||
|
// a network request.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5358
|
||||||
|
isEnvProduction &&
|
||||||
|
shouldInlineRuntimeChunk &&
|
||||||
|
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
|
||||||
|
// Makes some environment variables available in index.html.
|
||||||
|
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||||
|
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// It will be an empty string unless you specify "homepage"
|
||||||
|
// in `package.json`, in which case it will be the pathname of that URL.
|
||||||
|
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||||
|
// This gives some necessary context to module not found errors, such as
|
||||||
|
// the requesting resource.
|
||||||
|
new ModuleNotFoundPlugin(paths.appPath),
|
||||||
|
// Makes some environment variables available to the JS code, for example:
|
||||||
|
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||||
|
// It is absolutely essential that NODE_ENV is set to production
|
||||||
|
// during a production build.
|
||||||
|
// Otherwise React will be compiled in the very slow development mode.
|
||||||
|
new webpack.DefinePlugin(env.stringified),
|
||||||
|
// Experimental hot reloading for React .
|
||||||
|
// https://github.com/facebook/react/tree/main/packages/react-refresh
|
||||||
|
isEnvDevelopment &&
|
||||||
|
shouldUseReactRefresh &&
|
||||||
|
new ReactRefreshWebpackPlugin({
|
||||||
|
overlay: false,
|
||||||
|
}),
|
||||||
|
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||||
|
// a plugin that prints an error when you attempt to do this.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/240
|
||||||
|
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||||
|
isEnvProduction &&
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
// Options similar to the same options in webpackOptions.output
|
||||||
|
// both options are optional
|
||||||
|
filename: 'static/css/[name].[contenthash:8].css',
|
||||||
|
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
||||||
|
}),
|
||||||
|
// Generate an asset manifest file with the following content:
|
||||||
|
// - "files" key: Mapping of all asset filenames to their corresponding
|
||||||
|
// output file so that tools can pick it up without having to parse
|
||||||
|
// `index.html`
|
||||||
|
// - "entrypoints" key: Array of files which are included in `index.html`,
|
||||||
|
// can be used to reconstruct the HTML if necessary
|
||||||
|
new WebpackManifestPlugin({
|
||||||
|
fileName: 'asset-manifest.json',
|
||||||
|
publicPath: paths.publicUrlOrPath,
|
||||||
|
generate: (seed, files, entrypoints) => {
|
||||||
|
const manifestFiles = files.reduce((manifest, file) => {
|
||||||
|
manifest[file.name] = file.path;
|
||||||
|
return manifest;
|
||||||
|
}, seed);
|
||||||
|
const entrypointFiles = entrypoints.main.filter(fileName => !fileName.endsWith('.map'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: manifestFiles,
|
||||||
|
entrypoints: entrypointFiles,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Moment.js is an extremely popular library that bundles large locale files
|
||||||
|
// by default due to how webpack interprets its code. This is a practical
|
||||||
|
// solution that requires the user to opt into importing specific locales.
|
||||||
|
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||||
|
// You can remove this if you don't use Moment.js:
|
||||||
|
new webpack.IgnorePlugin({
|
||||||
|
resourceRegExp: /^\.\/locale$/,
|
||||||
|
contextRegExp: /moment$/,
|
||||||
|
}),
|
||||||
|
// Generate a service worker script that will precache, and keep up to date,
|
||||||
|
// the HTML & assets that are part of the webpack build.
|
||||||
|
isEnvProduction &&
|
||||||
|
fs.existsSync(swSrc) &&
|
||||||
|
new WorkboxWebpackPlugin.InjectManifest({
|
||||||
|
swSrc,
|
||||||
|
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
|
||||||
|
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
|
||||||
|
// Bump up the default maximum size (2mb) that's precached,
|
||||||
|
// to make lazy-loading failure scenarios less likely.
|
||||||
|
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
|
||||||
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
|
}),
|
||||||
|
// TypeScript type checking
|
||||||
|
useTypeScript &&
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
async: isEnvDevelopment,
|
||||||
|
typescript: {
|
||||||
|
typescriptPath: resolve.sync('typescript', {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}),
|
||||||
|
configOverwrite: {
|
||||||
|
compilerOptions: {
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
skipLibCheck: true,
|
||||||
|
inlineSourceMap: false,
|
||||||
|
declarationMap: false,
|
||||||
|
noEmit: true,
|
||||||
|
incremental: true,
|
||||||
|
tsBuildInfoFile: paths.appTsBuildInfoFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: paths.appPath,
|
||||||
|
diagnosticOptions: {
|
||||||
|
syntactic: true,
|
||||||
|
},
|
||||||
|
mode: 'write-references',
|
||||||
|
// profile: true,
|
||||||
|
},
|
||||||
|
issue: {
|
||||||
|
// This one is specifically to match during CI tests,
|
||||||
|
// as micromatch doesn't match
|
||||||
|
// '../cra-template-typescript/template/src/App.tsx'
|
||||||
|
// otherwise.
|
||||||
|
include: [{ file: '../**/src/**/*.{ts,tsx}' }, { file: '**/src/**/*.{ts,tsx}' }],
|
||||||
|
exclude: [
|
||||||
|
{ file: '**/src/**/__tests__/**' },
|
||||||
|
{ file: '**/src/**/?(*.){spec|test}.*' },
|
||||||
|
{ file: '**/src/setupProxy.*' },
|
||||||
|
{ file: '**/src/setupTests.*' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
infrastructure: 'silent',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
!disableESLintPlugin &&
|
||||||
|
new ESLintPlugin({
|
||||||
|
// Plugin options
|
||||||
|
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
|
||||||
|
formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||||
|
eslintPath: require.resolve('eslint'),
|
||||||
|
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
|
||||||
|
context: paths.appSrc,
|
||||||
|
cache: true,
|
||||||
|
cacheLocation: path.resolve(paths.appNodeModules, '.cache/.eslintcache'),
|
||||||
|
// ESLint class options
|
||||||
|
cwd: paths.appPath,
|
||||||
|
resolvePluginsRelativeTo: __dirname,
|
||||||
|
baseConfig: {
|
||||||
|
extends: [require.resolve('eslint-config-react-app/base')],
|
||||||
|
rules: {
|
||||||
|
...(!hasJsxRuntime && {
|
||||||
|
'react/react-in-jsx-scope': 'error',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
].filter(Boolean),
|
||||||
|
// Turn off performance processing because we utilize
|
||||||
|
// our own hints via the FileSizeReporter
|
||||||
|
performance: false,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
'use strict';
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
|
||||||
|
module.exports = env => {
|
||||||
|
const hash = createHash('md5');
|
||||||
|
hash.update(JSON.stringify(env));
|
||||||
|
|
||||||
|
return hash.digest('hex');
|
||||||
|
};
|
|
@ -0,0 +1,126 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||||
|
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||||
|
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||||
|
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const getHttpsConfig = require('./getHttpsConfig');
|
||||||
|
|
||||||
|
const host = process.env.HOST || '0.0.0.0';
|
||||||
|
const sockHost = process.env.WDS_SOCKET_HOST;
|
||||||
|
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/ws'
|
||||||
|
const sockPort = process.env.WDS_SOCKET_PORT;
|
||||||
|
|
||||||
|
module.exports = function(proxy, allowedHost) {
|
||||||
|
const disableFirewall = !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true';
|
||||||
|
return {
|
||||||
|
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||||
|
// websites from potentially accessing local content through DNS rebinding:
|
||||||
|
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||||
|
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||||
|
// However, it made several existing use cases such as development in cloud
|
||||||
|
// environment or subdomains in development significantly more complicated:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2271
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2233
|
||||||
|
// While we're investigating better solutions, for now we will take a
|
||||||
|
// compromise. Since our WDS configuration only serves files in the `public`
|
||||||
|
// folder we won't consider accessing them a vulnerability. However, if you
|
||||||
|
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||||
|
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||||
|
// So we will disable the host check normally, but enable it if you have
|
||||||
|
// specified the `proxy` setting. Finally, we let you override it if you
|
||||||
|
// really know what you're doing with a special environment variable.
|
||||||
|
// Note: ["localhost", ".localhost"] will support subdomains - but we might
|
||||||
|
// want to allow setting the allowedHosts manually for more complex setups
|
||||||
|
allowedHosts: disableFirewall ? 'all' : [allowedHost],
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': '*',
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
},
|
||||||
|
// Enable gzip compression of generated files.
|
||||||
|
compress: true,
|
||||||
|
static: {
|
||||||
|
// By default WebpackDevServer serves physical files from current directory
|
||||||
|
// in addition to all the virtual build products that it serves from memory.
|
||||||
|
// This is confusing because those files won’t automatically be available in
|
||||||
|
// production build folder unless we copy them. However, copying the whole
|
||||||
|
// project directory is dangerous because we may expose sensitive files.
|
||||||
|
// Instead, we establish a convention that only files in `public` directory
|
||||||
|
// get served. Our build script will copy `public` into the `build` folder.
|
||||||
|
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||||
|
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||||
|
// Note that we only recommend to use `public` folder as an escape hatch
|
||||||
|
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||||
|
// for some reason broken when imported through webpack. If you just want to
|
||||||
|
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||||
|
directory: paths.appPublic,
|
||||||
|
publicPath: [paths.publicUrlOrPath],
|
||||||
|
// By default files from `contentBase` will not trigger a page reload.
|
||||||
|
watch: {
|
||||||
|
// Reportedly, this avoids CPU overload on some systems.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/293
|
||||||
|
// src/node_modules is not ignored to support absolute imports
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1065
|
||||||
|
ignored: ignoredFiles(paths.appSrc),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
webSocketURL: {
|
||||||
|
// Enable custom sockjs pathname for websocket connection to hot reloading server.
|
||||||
|
// Enable custom sockjs hostname, pathname and port for websocket connection
|
||||||
|
// to hot reloading server.
|
||||||
|
hostname: sockHost,
|
||||||
|
pathname: sockPath,
|
||||||
|
port: sockPort,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
errors: true,
|
||||||
|
warnings: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
devMiddleware: {
|
||||||
|
// It is important to tell WebpackDevServer to use the same "publicPath" path as
|
||||||
|
// we specified in the webpack config. When homepage is '.', default to serving
|
||||||
|
// from the root.
|
||||||
|
// remove last slash so user can land on `/test` instead of `/test/`
|
||||||
|
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
||||||
|
},
|
||||||
|
|
||||||
|
https: getHttpsConfig(),
|
||||||
|
host,
|
||||||
|
historyApiFallback: {
|
||||||
|
// Paths with dots should still use the history fallback.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/387.
|
||||||
|
disableDotRule: true,
|
||||||
|
index: paths.publicUrlOrPath,
|
||||||
|
},
|
||||||
|
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
|
||||||
|
proxy,
|
||||||
|
onBeforeSetupMiddleware(devServer) {
|
||||||
|
// Keep `evalSourceMapMiddleware`
|
||||||
|
// middlewares before `redirectServedPath` otherwise will not have any effect
|
||||||
|
// This lets us fetch source contents from webpack for the error overlay
|
||||||
|
devServer.app.use(evalSourceMapMiddleware(devServer));
|
||||||
|
|
||||||
|
if (fs.existsSync(paths.proxySetup)) {
|
||||||
|
// This registers user provided middleware for proxy reasons
|
||||||
|
require(paths.proxySetup)(devServer.app);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAfterSetupMiddleware(devServer) {
|
||||||
|
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
|
||||||
|
devServer.app.use(redirectServedPath(paths.publicUrlOrPath));
|
||||||
|
|
||||||
|
// This service worker file is effectively a 'no-op' that will reset any
|
||||||
|
// previous service worker registered for the same host:port combination.
|
||||||
|
// We do this in development to avoid hitting the production cache if
|
||||||
|
// it used the same host and port.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||||
|
devServer.app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.js": "/common.min.js",
|
||||||
|
"main.css": "/static/css/main.92b513a3.css",
|
||||||
|
"index.html": "/index.html",
|
||||||
|
"common.min.js.map": "/common.min.js.map",
|
||||||
|
"main.92b513a3.css.map": "/static/css/main.92b513a3.css.map"
|
||||||
|
},
|
||||||
|
"entrypoints": [
|
||||||
|
"common.min.js",
|
||||||
|
"static/css/main.92b513a3.css"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
object-assign
|
||||||
|
(c) Sindre Sorhus
|
||||||
|
@license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Copyright (c) 2017 Jed Watson.
|
||||||
|
Licensed under the MIT License (MIT), see
|
||||||
|
http://jedwatson.github.io/classnames
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v0.19.1
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v16.13.1
|
||||||
|
* react-is.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v16.14.0
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v16.14.0
|
||||||
|
* react-jsx-runtime.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v16.14.0
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! moment.js
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="common.min.js"></script><link href="static/css/main.92b513a3.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,156 @@
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/common.min.js",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@elastic/datemath": "^5.0.3",
|
||||||
|
"antd": "^3.26.18",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"moment-timezone": "^0.5.32",
|
||||||
|
"react": "^16.14.0",
|
||||||
|
"react-dom": "^16.14.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node scripts/start.js",
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"package": "node scripts/package.js",
|
||||||
|
"prettier": "prettier -c --write \"**/*\""
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.16.0",
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
||||||
|
"@svgr/webpack": "^5.5.0",
|
||||||
|
"babel-jest": "^27.4.2",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
|
"babel-plugin-import": "^1.13.8",
|
||||||
|
"babel-plugin-named-asset-import": "^0.3.8",
|
||||||
|
"babel-preset-react-app": "^10.0.1",
|
||||||
|
"bfj": "^7.0.2",
|
||||||
|
"browserslist": "^4.18.1",
|
||||||
|
"camelcase": "^6.2.1",
|
||||||
|
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||||
|
"css-loader": "^6.5.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^3.2.0",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"dotenv-expand": "^5.1.0",
|
||||||
|
"eslint": "^8.3.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fs-extra": "^10.0.0",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^27.4.3",
|
||||||
|
"jest-resolve": "^27.4.2",
|
||||||
|
"jest-watch-typeahead": "^1.0.0",
|
||||||
|
"less": "^3.13.1",
|
||||||
|
"less-loader": "^11.1.3",
|
||||||
|
"mini-css-extract-plugin": "^2.4.5",
|
||||||
|
"mutationobserver-shim": "0.3.3",
|
||||||
|
"postcss": "^8.4.4",
|
||||||
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
|
"postcss-loader": "^6.2.1",
|
||||||
|
"postcss-normalize": "^10.0.1",
|
||||||
|
"postcss-preset-env": "^7.0.1",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"react-app-polyfill": "^3.0.0",
|
||||||
|
"react-dev-utils": "^12.0.1",
|
||||||
|
"react-refresh": "^0.11.0",
|
||||||
|
"resolve": "^1.20.0",
|
||||||
|
"resolve-url-loader": "^4.0.0",
|
||||||
|
"sass-loader": "^12.3.0",
|
||||||
|
"semver": "^7.3.5",
|
||||||
|
"source-map-loader": "^3.0.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"tailwindcss": "^3.0.2",
|
||||||
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
|
"webpack": "^5.64.4",
|
||||||
|
"webpack-dev-server": "^4.6.0",
|
||||||
|
"webpack-manifest-plugin": "^4.0.2",
|
||||||
|
"workbox-webpack-plugin": "^6.4.1"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"roots": [
|
||||||
|
"<rootDir>/src"
|
||||||
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"react-app-polyfill/jsdom"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [],
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
|
||||||
|
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||||
|
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$"
|
||||||
|
],
|
||||||
|
"modulePaths": [],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^react-native$": "react-native-web",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"web.js",
|
||||||
|
"js",
|
||||||
|
"web.ts",
|
||||||
|
"ts",
|
||||||
|
"web.tsx",
|
||||||
|
"tsx",
|
||||||
|
"json",
|
||||||
|
"web.jsx",
|
||||||
|
"jsx",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"watchPlugins": [
|
||||||
|
"jest-watch-typeahead/filename",
|
||||||
|
"jest-watch-typeahead/testname"
|
||||||
|
],
|
||||||
|
"resetMocks": true
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"react-app"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"import",
|
||||||
|
{
|
||||||
|
"libraryName": "antd",
|
||||||
|
"style": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -0,0 +1,205 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'production';
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const bfj = require('bfj');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||||
|
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||||
|
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||||
|
const printBuildError = require('react-dev-utils/printBuildError');
|
||||||
|
|
||||||
|
const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
|
||||||
|
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
|
||||||
|
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||||
|
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||||
|
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||||
|
|
||||||
|
// Generate configuration
|
||||||
|
const config = configFactory('production');
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// First, read the current file sizes in build directory.
|
||||||
|
// This lets us display how much they changed later.
|
||||||
|
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||||
|
})
|
||||||
|
.then(previousFileSizes => {
|
||||||
|
// Remove all content but keep the directory so that
|
||||||
|
// if you're in it, you don't end up in Trash
|
||||||
|
fs.emptyDirSync(paths.appBuild);
|
||||||
|
// Merge with the public folder
|
||||||
|
copyPublicFolder();
|
||||||
|
// Start the webpack build
|
||||||
|
return build(previousFileSizes);
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
({ stats, previousFileSizes, warnings }) => {
|
||||||
|
if (warnings.length) {
|
||||||
|
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||||
|
console.log(warnings.join('\n\n'));
|
||||||
|
console.log(
|
||||||
|
'\nSearch for the ' +
|
||||||
|
chalk.underline(chalk.yellow('keywords')) +
|
||||||
|
' to learn more about each warning.',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'To ignore, add ' + chalk.cyan('// eslint-disable-next-line') + ' to the line before.\n',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.green('Compiled successfully.\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('File sizes after gzip:\n');
|
||||||
|
printFileSizesAfterBuild(
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
paths.appBuild,
|
||||||
|
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||||
|
WARN_AFTER_CHUNK_GZIP_SIZE,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const appPackage = require(paths.appPackageJson);
|
||||||
|
const publicUrl = paths.publicUrlOrPath;
|
||||||
|
const publicPath = config.output.publicPath;
|
||||||
|
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||||
|
printHostingInstructions(appPackage, publicUrl, publicPath, buildFolder, useYarn);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||||
|
if (tscCompileOnError) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'Compiled with the following type errors (you may want to check these before deploying your app):\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
printBuildError(err);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red('Failed to compile.\n'));
|
||||||
|
printBuildError(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the production build and print the deployment instructions.
|
||||||
|
function build(previousFileSizes) {
|
||||||
|
console.log('Creating an optimized production build...');
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compiler.run((err, stats) => {
|
||||||
|
let messages;
|
||||||
|
if (err) {
|
||||||
|
if (!err.message) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errMessage = err.message;
|
||||||
|
|
||||||
|
// Add additional information for postcss errors
|
||||||
|
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||||
|
errMessage += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = formatWebpackMessages({
|
||||||
|
errors: [errMessage],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages = formatWebpackMessages(
|
||||||
|
stats.toJson({ all: false, warnings: true, errors: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (messages.errors.length) {
|
||||||
|
// Only keep the first error. Others are often indicative
|
||||||
|
// of the same problem, but confuse the reader with noise.
|
||||||
|
if (messages.errors.length > 1) {
|
||||||
|
messages.errors.length = 1;
|
||||||
|
}
|
||||||
|
return reject(new Error(messages.errors.join('\n\n')));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
process.env.CI &&
|
||||||
|
(typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') &&
|
||||||
|
messages.warnings.length
|
||||||
|
) {
|
||||||
|
// Ignore sourcemap warnings in CI builds. See #8227 for more info.
|
||||||
|
const filteredWarnings = messages.warnings.filter(
|
||||||
|
w => !/Failed to parse source map/.test(w),
|
||||||
|
);
|
||||||
|
if (filteredWarnings.length) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||||
|
'Most CI servers set it automatically.\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return reject(new Error(filteredWarnings.join('\n\n')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveArgs = {
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
warnings: messages.warnings,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (writeStatsJson) {
|
||||||
|
return bfj
|
||||||
|
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||||
|
.then(() => resolve(resolveArgs))
|
||||||
|
.catch(error => reject(new Error(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(resolveArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPublicFolder() {
|
||||||
|
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||||
|
dereference: true,
|
||||||
|
filter: file => file !== paths.appHtml,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'production';
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const bfj = require('bfj');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||||
|
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||||
|
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||||
|
const printBuildError = require('react-dev-utils/printBuildError');
|
||||||
|
|
||||||
|
const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
|
||||||
|
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
|
||||||
|
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||||
|
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||||
|
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||||
|
|
||||||
|
// Generate configuration
|
||||||
|
const config = configFactory('package');
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// First, read the current file sizes in build directory.
|
||||||
|
// This lets us display how much they changed later.
|
||||||
|
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||||
|
})
|
||||||
|
.then(previousFileSizes => {
|
||||||
|
// Remove all content but keep the directory so that
|
||||||
|
// if you're in it, you don't end up in Trash
|
||||||
|
fs.emptyDirSync(paths.appBuild);
|
||||||
|
// Merge with the public folder
|
||||||
|
copyPublicFolder();
|
||||||
|
// Start the webpack build
|
||||||
|
return build(previousFileSizes);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the production build and print the deployment instructions.
|
||||||
|
function build(previousFileSizes) {
|
||||||
|
console.log('Creating an optimized production build...');
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compiler.run((err, stats) => {
|
||||||
|
let messages;
|
||||||
|
if (err) {
|
||||||
|
if (!err.message) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errMessage = err.message;
|
||||||
|
|
||||||
|
// Add additional information for postcss errors
|
||||||
|
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||||
|
errMessage += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = formatWebpackMessages({
|
||||||
|
errors: [errMessage],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages = formatWebpackMessages(
|
||||||
|
stats.toJson({ all: false, warnings: true, errors: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (messages.errors.length) {
|
||||||
|
// Only keep the first error. Others are often indicative
|
||||||
|
// of the same problem, but confuse the reader with noise.
|
||||||
|
if (messages.errors.length > 1) {
|
||||||
|
messages.errors.length = 1;
|
||||||
|
}
|
||||||
|
return reject(new Error(messages.errors.join('\n\n')));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
process.env.CI &&
|
||||||
|
(typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') &&
|
||||||
|
messages.warnings.length
|
||||||
|
) {
|
||||||
|
// Ignore sourcemap warnings in CI builds. See #8227 for more info.
|
||||||
|
const filteredWarnings = messages.warnings.filter(
|
||||||
|
w => !/Failed to parse source map/.test(w),
|
||||||
|
);
|
||||||
|
if (filteredWarnings.length) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||||
|
'Most CI servers set it automatically.\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return reject(new Error(filteredWarnings.join('\n\n')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveArgs = {
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
warnings: messages.warnings,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (writeStatsJson) {
|
||||||
|
return bfj
|
||||||
|
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||||
|
.then(() => resolve(resolveArgs))
|
||||||
|
.catch(error => reject(new Error(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(resolveArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPublicFolder() {
|
||||||
|
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||||
|
dereference: true,
|
||||||
|
filter: file => file !== paths.appHtml,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'development';
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const WebpackDevServer = require('webpack-dev-server');
|
||||||
|
const clearConsole = require('react-dev-utils/clearConsole');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const {
|
||||||
|
choosePort,
|
||||||
|
createCompiler,
|
||||||
|
prepareProxy,
|
||||||
|
prepareUrls,
|
||||||
|
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||||
|
const openBrowser = require('react-dev-utils/openBrowser');
|
||||||
|
const semver = require('semver');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||||
|
const getClientEnvironment = require('../config/env');
|
||||||
|
const react = require(require.resolve('react', { paths: [paths.appPath] }));
|
||||||
|
|
||||||
|
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools like Cloud9 rely on this.
|
||||||
|
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||||
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
|
if (process.env.HOST) {
|
||||||
|
console.log(
|
||||||
|
chalk.cyan(
|
||||||
|
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||||
|
chalk.bold(process.env.HOST),
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log(`If this was unintentional, check that you haven't mistakenly set it in your shell.`);
|
||||||
|
console.log(`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// We attempt to use the default port but if it is busy, we offer the user to
|
||||||
|
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||||
|
return choosePort(HOST, DEFAULT_PORT);
|
||||||
|
})
|
||||||
|
.then(port => {
|
||||||
|
if (port == null) {
|
||||||
|
// We have not found a port.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = configFactory('development');
|
||||||
|
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||||
|
const appName = require(paths.appPackageJson).name;
|
||||||
|
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
const urls = prepareUrls(protocol, HOST, port, paths.publicUrlOrPath.slice(0, -1));
|
||||||
|
// Create a webpack compiler that is configured with custom messages.
|
||||||
|
const compiler = createCompiler({
|
||||||
|
appName,
|
||||||
|
config,
|
||||||
|
urls,
|
||||||
|
useYarn,
|
||||||
|
useTypeScript,
|
||||||
|
webpack,
|
||||||
|
});
|
||||||
|
// Load proxy config
|
||||||
|
const proxySetting = require(paths.appPackageJson).proxy;
|
||||||
|
const proxyConfig = prepareProxy(proxySetting, paths.appPublic, paths.publicUrlOrPath);
|
||||||
|
// Serve webpack assets generated by the compiler over a web server.
|
||||||
|
const serverConfig = {
|
||||||
|
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
|
||||||
|
host: HOST,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
const devServer = new WebpackDevServer(serverConfig, compiler);
|
||||||
|
// Launch WebpackDevServer.
|
||||||
|
devServer.startCallback(() => {
|
||||||
|
if (isInteractive) {
|
||||||
|
clearConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.cyan('Starting the development server...\n'));
|
||||||
|
openBrowser(urls.localUrlForBrowser);
|
||||||
|
});
|
||||||
|
|
||||||
|
['SIGINT', 'SIGTERM'].forEach(function(sig) {
|
||||||
|
process.on(sig, function() {
|
||||||
|
devServer.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.CI !== 'true') {
|
||||||
|
// Gracefully exit when stdin ends
|
||||||
|
process.stdin.on('end', function() {
|
||||||
|
devServer.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'test';
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.PUBLIC_URL = '';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
const jest = require('jest');
|
||||||
|
const execSync = require('child_process').execSync;
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
function isInGitRepository() {
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInMercurialRepository() {
|
||||||
|
try {
|
||||||
|
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch unless on CI or explicitly running all tests
|
||||||
|
if (
|
||||||
|
!process.env.CI &&
|
||||||
|
argv.indexOf('--watchAll') === -1 &&
|
||||||
|
argv.indexOf('--watchAll=false') === -1
|
||||||
|
) {
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5210
|
||||||
|
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||||
|
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.run(argv);
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import styles from './Apply.less';
|
||||||
|
|
||||||
|
const Apply = props => {
|
||||||
|
const { currentLocales, onApply, onCancel } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.apply}>
|
||||||
|
<Button type="link" onClick={onCancel}>
|
||||||
|
{currentLocales[`datepicker.cancel`]}
|
||||||
|
</Button>
|
||||||
|
<Button className={styles.applyBtn} type="primary" onClick={onApply}>
|
||||||
|
{currentLocales[`datepicker.apply`]}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Apply;
|
|
@ -0,0 +1,5 @@
|
||||||
|
.apply {
|
||||||
|
.applyBtn {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import DatePicker from '.';
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
const [range, setRange] = useState({ start: 'now-15m', end: 'now' });
|
||||||
|
|
||||||
|
const [refresh, setRefresh] = useState({ isRefreshPaused: true, refreshInterval: 10000 });
|
||||||
|
|
||||||
|
const [autoFitLoading, setAutoFitLoading] = useState(false);
|
||||||
|
|
||||||
|
const [timeSetting, setTimeSetting] = useState({
|
||||||
|
showTimeSetting: true,
|
||||||
|
showTimeField: true,
|
||||||
|
timeField: 'timestamp',
|
||||||
|
timeFields: [
|
||||||
|
'payload.elasticsearch.index_routing_table.shards.0.unassigned_info.at',
|
||||||
|
'timestamp',
|
||||||
|
],
|
||||||
|
showTimeInterval: true,
|
||||||
|
timeInterval: '15s',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [timeZone, setTimeZone] = useState('Asia/Shanghai');
|
||||||
|
|
||||||
|
const onAutoFit = () => {
|
||||||
|
setAutoFitLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setRange({ start: '2023-09-09 09:09:09', end: '2023-10-10 10:10:10' });
|
||||||
|
setAutoFitLoading(false);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', maxWidth: 480 }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<DatePicker
|
||||||
|
{...range}
|
||||||
|
onRangeChange={({ start, end }) => {
|
||||||
|
setRange({ start, end })
|
||||||
|
}}
|
||||||
|
{...refresh}
|
||||||
|
onRefreshChange={setRefresh}
|
||||||
|
{...timeSetting}
|
||||||
|
onTimeSettingChange={newTimeSetting => {
|
||||||
|
setTimeSetting({ ...timeSetting, ...newTimeSetting });
|
||||||
|
}}
|
||||||
|
autoFitLoading={autoFitLoading}
|
||||||
|
onAutoFit={onAutoFit}
|
||||||
|
timeZone={timeZone}
|
||||||
|
onTimeZoneChange={setTimeZone}
|
||||||
|
recentlyUsedRangesKey={"demo-recently-used-ranges"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Demo;
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { InputNumber, Select } from 'antd';
|
||||||
|
import { timeUnits } from './utils/time_units';
|
||||||
|
import { parseTimeParts } from './utils/quick_select_utils';
|
||||||
|
import styles from './QuickSelect.less';
|
||||||
|
import RefreshInterval from './RefreshInterval';
|
||||||
|
import Apply from './Apply';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const LAST = 'last';
|
||||||
|
|
||||||
|
const timeTenseOptions = [
|
||||||
|
{ value: LAST, text: 'Last' },
|
||||||
|
];
|
||||||
|
const timeUnitsOptions = Object.keys(timeUnits).map(key => {
|
||||||
|
return { value: key, text: `${timeUnits[key]}s` };
|
||||||
|
});
|
||||||
|
|
||||||
|
const QuickSelect = props => {
|
||||||
|
const { currentLocales, start, end, prevQuickSelect, onRangeChange, onCancel } = props;
|
||||||
|
|
||||||
|
const [time, setTime] = useState(() => {
|
||||||
|
const {
|
||||||
|
timeTense: timeTenseDefault,
|
||||||
|
timeUnits: timeUnitsDefault,
|
||||||
|
timeValue: timeValueDefault,
|
||||||
|
} = parseTimeParts(start, end);
|
||||||
|
return {
|
||||||
|
timeTense: prevQuickSelect?.timeTense || timeTenseDefault,
|
||||||
|
timeValue: prevQuickSelect?.timeValue || timeValueDefault,
|
||||||
|
timeUnits: prevQuickSelect?.timeUnits || timeUnitsDefault,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { timeTense, timeUnits, timeValue } = time;
|
||||||
|
|
||||||
|
const onTimeTenseChange = value => {
|
||||||
|
setTime({
|
||||||
|
...time,
|
||||||
|
timeTense: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeValueChange = value => {
|
||||||
|
setTime({
|
||||||
|
...time,
|
||||||
|
timeValue: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeUnitsChange = value => {
|
||||||
|
setTime({
|
||||||
|
...time,
|
||||||
|
timeUnits: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
const { timeValue, timeUnits } = time;
|
||||||
|
|
||||||
|
onRangeChange({
|
||||||
|
start: `now-${timeValue}${timeUnits}`,
|
||||||
|
end: 'now',
|
||||||
|
quickSelect: time,
|
||||||
|
});
|
||||||
|
onCancel()
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.quickSelect}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.quick_select`]}</div>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<Select value={timeTense} onChange={onTimeTenseChange} style={{ width: '100%' }}>
|
||||||
|
{timeTenseOptions.map(item => (
|
||||||
|
<Select.Option key={item.value} value={item.value}>
|
||||||
|
{currentLocales[`datepicker.quick_select.${item.value}`]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
value={timeValue}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={onTimeValueChange}
|
||||||
|
/>
|
||||||
|
<Select value={timeUnits} onChange={onTimeUnitsChange} style={{ width: '100%' }}>
|
||||||
|
{timeUnitsOptions.map(item => (
|
||||||
|
<Select.Option key={item.value} value={item.value}>
|
||||||
|
{currentLocales[`datepicker.time.units.${item.value}`]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.apply}>
|
||||||
|
<Apply currentLocales={currentLocales} onApply={handleApply} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.refreshInterval}>
|
||||||
|
<RefreshInterval
|
||||||
|
currentLocales={currentLocales}
|
||||||
|
isRefreshPaused={props.isRefreshPaused}
|
||||||
|
refreshInterval={props.refreshInterval}
|
||||||
|
onRefreshChange={props.onRefreshChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickSelect;
|
|
@ -0,0 +1,30 @@
|
||||||
|
.quickSelect {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshInterval {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,285 @@
|
||||||
|
import { Button, Icon, Popover, Spin } from "antd";
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
|
import QuickSelect from "./QuickSelect";
|
||||||
|
import StartAndEndTimes from "./StartAndEndTimes";
|
||||||
|
import TimeSetting from "./TimeSetting";
|
||||||
|
import TimeZone from "./TimeZone";
|
||||||
|
import {
|
||||||
|
getDateString,
|
||||||
|
getDateStringWithGMT,
|
||||||
|
getGMTString,
|
||||||
|
} from "./utils/utils";
|
||||||
|
import { AsyncInterval } from "./utils/async_interval";
|
||||||
|
import { prettyDuration, showPrettyDuration } from "./utils/pretty_duration";
|
||||||
|
|
||||||
|
import styles from "./Range.less";
|
||||||
|
|
||||||
|
const TIME_ZONE_KEY = "time_zone";
|
||||||
|
const TIME_SETTING_KEY = "time_setting";
|
||||||
|
|
||||||
|
const SETTING = [
|
||||||
|
{
|
||||||
|
key: "quick_select",
|
||||||
|
label: "Quick select",
|
||||||
|
icon: <Icon className={styles.icon} type="thunderbolt" />,
|
||||||
|
component: QuickSelect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "start_and_end_times",
|
||||||
|
label: "Start and end times",
|
||||||
|
icon: <Icon className={styles.icon} type="calendar" />,
|
||||||
|
component: StartAndEndTimes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TIME_SETTING_KEY,
|
||||||
|
label: "Time setting",
|
||||||
|
icon: <Icon className={styles.icon} type="setting" />,
|
||||||
|
component: TimeSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TIME_ZONE_KEY,
|
||||||
|
label: "Time zone",
|
||||||
|
render: (item, { timeZone }, locales) =>
|
||||||
|
`${locales[`datepicker.${item.key}`]} | ${getGMTString(timeZone)}`,
|
||||||
|
icon: <Icon className={styles.icon} type="global" />,
|
||||||
|
component: TimeZone,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Range = (props) => {
|
||||||
|
const {
|
||||||
|
currentLocales,
|
||||||
|
popoverClassName,
|
||||||
|
popoverPlacement,
|
||||||
|
dateFormat,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
onRangeChange,
|
||||||
|
commonlyUsedRanges,
|
||||||
|
isRefreshPaused,
|
||||||
|
refreshInterval,
|
||||||
|
onRefreshChange,
|
||||||
|
onRefresh,
|
||||||
|
showTimeSetting,
|
||||||
|
autoFitLoading,
|
||||||
|
onAutoFit,
|
||||||
|
timeZone,
|
||||||
|
isMinimum,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
useImperativeHandle(props.onRef, () => {
|
||||||
|
return {
|
||||||
|
handleRefreshChange: handleRefreshChange,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshIntervalRef = useRef(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [showMenuIcon, setShowMenuIcon] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState();
|
||||||
|
const [height, setHeight] = useState();
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
const rangeRef = useRef({ start, end });
|
||||||
|
|
||||||
|
const handleSettingClick = (item) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setShowMenuIcon(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisible = (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
setVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
setHeight(contentRef.current.offsetHeight);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setVisible(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedItem();
|
||||||
|
setShowMenuIcon(false);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshChange = ({ isRefreshPaused, refreshInterval }) => {
|
||||||
|
stopInterval();
|
||||||
|
if (onRefreshChange) {
|
||||||
|
onRefreshChange({ refreshInterval, isRefreshPaused });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopInterval = () => {
|
||||||
|
if (refreshIntervalRef.current) {
|
||||||
|
refreshIntervalRef.current.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startInterval = (refreshInterval, onRefresh) => {
|
||||||
|
stopInterval();
|
||||||
|
if (onRefresh && rangeRef.current) {
|
||||||
|
const handler = () => {
|
||||||
|
onRefresh({ ...rangeRef.current, refreshInterval });
|
||||||
|
};
|
||||||
|
refreshIntervalRef.current = new AsyncInterval(handler, refreshInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopInterval();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRefreshPaused) {
|
||||||
|
startInterval(refreshInterval, onRefresh);
|
||||||
|
} else {
|
||||||
|
stopInterval();
|
||||||
|
}
|
||||||
|
}, [refreshInterval, isRefreshPaused, onRefresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rangeRef.current = { start, end };
|
||||||
|
}, [start, end]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div ref={contentRef} className={styles.rangeSetting}>
|
||||||
|
<div className={styles.menu}>
|
||||||
|
<div className={styles.quickSelect}>
|
||||||
|
{onAutoFit && (
|
||||||
|
<Spin size="small" spinning={!!autoFitLoading}>
|
||||||
|
<div
|
||||||
|
className={`${styles.item} ${isMinimum ? styles.disabled : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
onAutoFit();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLocales[`datepicker.quick_select.auto_fit`]}
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
)}
|
||||||
|
{commonlyUsedRanges.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`${styles.item} ${isMinimum ? styles.disabled : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
onRangeChange({ start: item.start, end: item.end });
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLocales[`datepicker.quick_select.${item.key}`]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.setting}>
|
||||||
|
{SETTING.filter(
|
||||||
|
(item) => item.key !== TIME_SETTING_KEY || !!showTimeSetting
|
||||||
|
).map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className={`${styles.item} ${
|
||||||
|
selectedItem?.key === item.key ? styles.selected : ""
|
||||||
|
} ${
|
||||||
|
isMinimum &&
|
||||||
|
![TIME_ZONE_KEY, TIME_SETTING_KEY].includes(item.key)
|
||||||
|
? styles.disabled
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSettingClick(item)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{showMenuIcon && item.icon && (
|
||||||
|
<span className={styles.icon}>{item.icon}</span>
|
||||||
|
)}
|
||||||
|
{item.render
|
||||||
|
? item.render(item, props, currentLocales)
|
||||||
|
: currentLocales[`datepicker.${item.key}`]}
|
||||||
|
</div>
|
||||||
|
<Icon className={styles.right} type="right" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedItem?.component && (
|
||||||
|
<div className={styles.content} style={{ height: height || "100%" }}>
|
||||||
|
<selectedItem.component
|
||||||
|
{...props}
|
||||||
|
onRefreshChange={handleRefreshChange}
|
||||||
|
onCancel={handleClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullRangeText = useMemo(() => {
|
||||||
|
if (isMinimum || !start || !end) return "";
|
||||||
|
if (showPrettyDuration(start, end, commonlyUsedRanges)) {
|
||||||
|
return prettyDuration(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
commonlyUsedRanges,
|
||||||
|
dateFormat,
|
||||||
|
currentLocales
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return `${getDateString(
|
||||||
|
start,
|
||||||
|
timeZone,
|
||||||
|
dateFormat
|
||||||
|
)} ~ ${getDateStringWithGMT(end, timeZone, dateFormat)}`;
|
||||||
|
}
|
||||||
|
}, [isMinimum, start, end, commonlyUsedRanges, dateFormat, timeZone]);
|
||||||
|
|
||||||
|
const rangeText = useMemo(() => {
|
||||||
|
const now = moment().tz(timeZone);
|
||||||
|
const dateString = now.format("YYYY-MM-DD");
|
||||||
|
const yearString = now.format("YYYY");
|
||||||
|
return typeof fullRangeText === "string"
|
||||||
|
? fullRangeText
|
||||||
|
.replaceAll(`${dateString}`, "")
|
||||||
|
.replaceAll(`${yearString}-`, "")
|
||||||
|
: "";
|
||||||
|
}, [fullRangeText, timeZone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={handleVisible}
|
||||||
|
placement={popoverPlacement}
|
||||||
|
content={content}
|
||||||
|
trigger={"click"}
|
||||||
|
overlayClassName={`${styles.popover} ${popoverClassName}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
title={fullRangeText}
|
||||||
|
className={`${styles.rangeBtn} ${
|
||||||
|
isMinimum ? styles.minimum : ""
|
||||||
|
} common-ui-datepicker-range`}
|
||||||
|
>
|
||||||
|
<div className={styles.rangeContent}>
|
||||||
|
<Icon className={styles.clock} type="clock-circle" />
|
||||||
|
<span className={styles.label}>{rangeText}</span>
|
||||||
|
<Icon className={styles.down} type="down" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Range;
|
|
@ -0,0 +1,108 @@
|
||||||
|
.rangeBtn {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeContent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.minimum {
|
||||||
|
width: 48px;
|
||||||
|
|
||||||
|
.rangeContent {
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
:global {
|
||||||
|
.ant-popover-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeSetting {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
min-width: 220px;
|
||||||
|
.item {
|
||||||
|
height: 32px;
|
||||||
|
color: #666;
|
||||||
|
padding: 0 9px 0 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: #dbdbdb;
|
||||||
|
background-color: #fff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #101010;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickSelect {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
border-left: 1px solid #ebebeb;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Button, Icon, InputNumber, Select } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { timeUnits } from "./utils/time_units";
|
||||||
|
import { toMilliseconds, fromMilliseconds } from "./utils/utils";
|
||||||
|
|
||||||
|
import styles from "./RefreshInterval.less";
|
||||||
|
|
||||||
|
const timeUnitsOptions = Object.keys(timeUnits).map((key) => {
|
||||||
|
return { value: key, text: `${timeUnits[key]}s` };
|
||||||
|
});
|
||||||
|
|
||||||
|
const RefreshInterval = (props) => {
|
||||||
|
const {
|
||||||
|
currentLocales,
|
||||||
|
isRefreshPaused,
|
||||||
|
refreshInterval = 10000,
|
||||||
|
onRefreshChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [time, setTime] = useState(() => fromMilliseconds(refreshInterval));
|
||||||
|
const { value, units } = time;
|
||||||
|
|
||||||
|
const onValueChange = (value) => {
|
||||||
|
setTime({ ...time, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUnitsChange = (units) => {
|
||||||
|
setTime({ ...time, units });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPlayClick = () => {
|
||||||
|
onRefreshChange({
|
||||||
|
refreshInterval: toMilliseconds(units, value),
|
||||||
|
isRefreshPaused: !isRefreshPaused,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.refreshInterval}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{currentLocales["datepicker.refresh_every"]}
|
||||||
|
</div>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
value={value}
|
||||||
|
className={styles.value}
|
||||||
|
onChange={onValueChange}
|
||||||
|
/>
|
||||||
|
<Select value={units} className={styles.units} onChange={onUnitsChange}>
|
||||||
|
{timeUnitsOptions.map((item) => (
|
||||||
|
<Select.Option key={item.value} value={item.value}>
|
||||||
|
{currentLocales[`datepicker.time.units.${item.value}`]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button className={styles.play} type="primary" onClick={onPlayClick}>
|
||||||
|
{isRefreshPaused ? (
|
||||||
|
<Icon type="caret-right" />
|
||||||
|
) : (
|
||||||
|
<Icon type="pause" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RefreshInterval;
|
|
@ -0,0 +1,29 @@
|
||||||
|
.refreshInterval {
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 70%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.units {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play {
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 32px;
|
||||||
|
padding: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import Apply from './Apply';
|
||||||
|
import RefreshInterval from './RefreshInterval';
|
||||||
|
import styles from './StartAndEndTimes.less';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import dateMath from '@elastic/datemath';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { getDateString, getDateStringWithGMT } from './utils/utils';
|
||||||
|
|
||||||
|
function isRangeInvalid(start, end) {
|
||||||
|
const startMoment = dateMath.parse(start);
|
||||||
|
const endMoment = dateMath.parse(end, { roundUp: true });
|
||||||
|
if (!startMoment || !endMoment || !startMoment.isValid() || !endMoment.isValid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (startMoment.isAfter(endMoment)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StartAndEndTimes = props => {
|
||||||
|
const {
|
||||||
|
currentLocales,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
onRangeChange,
|
||||||
|
onCancel,
|
||||||
|
dateFormat,
|
||||||
|
timeZone,
|
||||||
|
recentlyUsedRanges = [],
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [startAsMoment, setStartAsMoment] = useState(() => {
|
||||||
|
const value = dateMath.parse(start);
|
||||||
|
return value && value.isValid() ? value.tz(timeZone) : moment().tz(timeZone);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isInvalid, setIsInvalid] = useState(false)
|
||||||
|
|
||||||
|
const [endAsMoment, setEndAsMoment] = useState(() => {
|
||||||
|
const value = dateMath.parse(end);
|
||||||
|
return value && value.isValid() ? value.tz(timeZone) : moment().tz(timeZone);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
const startValue = startAsMoment.toISOString(true);
|
||||||
|
const endValue = endAsMoment.toISOString(true);
|
||||||
|
const isInvalid = isRangeInvalid(startValue, endValue);
|
||||||
|
if (!isInvalid) {
|
||||||
|
onRangeChange({ start: startValue, end: endValue, isAbsolute: true });
|
||||||
|
onCancel()
|
||||||
|
} else {
|
||||||
|
setIsInvalid(true)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.startAndEndTimes}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.start_and_end_times`]}{` ${startAsMoment.format('(G[M]TZ)')}`}</div>
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<div className={styles.label}>{currentLocales[`datepicker.start_and_end_times.start_time`]}</div>
|
||||||
|
<DatePicker
|
||||||
|
format={dateFormat}
|
||||||
|
value={startAsMoment}
|
||||||
|
className={styles.datePicker}
|
||||||
|
showTime
|
||||||
|
allowClear={false}
|
||||||
|
onChange={date => setStartAsMoment(date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<div className={styles.label}>{currentLocales[`datepicker.start_and_end_times.end_time`]}</div>
|
||||||
|
<DatePicker
|
||||||
|
format={dateFormat}
|
||||||
|
value={endAsMoment}
|
||||||
|
className={styles.datePicker}
|
||||||
|
showTime
|
||||||
|
allowClear={false}
|
||||||
|
onChange={date => setEndAsMoment(date)}
|
||||||
|
/>
|
||||||
|
{isInvalid && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
{currentLocales[`datepicker.start_and_end_times.end_time`]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.apply}>
|
||||||
|
<Apply currentLocales={currentLocales} onApply={handleApply} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
recentlyUsedRanges.length > 0 && (
|
||||||
|
<div className={styles.recent}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.start_and_end_times.recent`]}</div>
|
||||||
|
{recentlyUsedRanges.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles.item}
|
||||||
|
onClick={() => {
|
||||||
|
onRangeChange({
|
||||||
|
start: moment(item.start).tz(item.timeZone).tz(timeZone).toISOString(true),
|
||||||
|
end: moment(item.end).tz(item.timeZone).tz(timeZone).toISOString(true)
|
||||||
|
})
|
||||||
|
onCancel()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getDateString(item.start, item.timeZone, dateFormat)} ~{' '}
|
||||||
|
{getDateStringWithGMT(item.end, item.timeZone, dateFormat)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div className={styles.refreshInterval}>
|
||||||
|
<RefreshInterval
|
||||||
|
currentLocales={currentLocales}
|
||||||
|
isRefreshPaused={props.isRefreshPaused}
|
||||||
|
refreshInterval={props.refreshInterval}
|
||||||
|
onRefreshChange={props.onRefreshChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartAndEndTimes;
|
|
@ -0,0 +1,81 @@
|
||||||
|
.startAndEndTimes {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formItem {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datePicker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #f5222d;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent {
|
||||||
|
height: calc(100% - 30px - 25px - 32px - 12px - 25px - 32px - 12px - 32px - 16px);
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
padding: 12px 0;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name,
|
||||||
|
.gmt {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshInterval {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
:global {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { InputNumber, Select, Switch } from 'antd';
|
||||||
|
import Apply from './Apply';
|
||||||
|
import styles from './TimeSetting.less';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const timeIntervals = [
|
||||||
|
// { label: 'Millisecond', value: 'ms' },
|
||||||
|
{ label: 'Second', value: 's' },
|
||||||
|
{ label: 'Minute', value: 'm' },
|
||||||
|
{ label: 'Hour', value: 'h' },
|
||||||
|
{ label: 'Day', value: 'd' },
|
||||||
|
{ label: 'Week', value: 'w' },
|
||||||
|
{ label: 'Month', value: 'M' },
|
||||||
|
{ label: 'Year', value: 'y' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TimeSetting = props => {
|
||||||
|
const { currentLocales, timeFields = [], showTimeField, showTimeInterval, onTimeSettingChange, onCancel } = props;
|
||||||
|
|
||||||
|
const [isAuto, setIsAuto] = useState(!props.timeInterval)
|
||||||
|
const [timeField, setTimeField] = useState(props.timeField);
|
||||||
|
const [timeInterval, setTimeInterval] = useState(props.timeInterval);
|
||||||
|
const timeIntervalCache = useRef('');
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onTimeSettingChange({ timeField, timeInterval });
|
||||||
|
onCancel()
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeIntervalObject = useMemo(() => {
|
||||||
|
if (!timeInterval) return
|
||||||
|
const value = parseInt(timeInterval);
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
unit: timeInterval.replace(`${value}`, ''),
|
||||||
|
}
|
||||||
|
}, [timeInterval])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.timeSetting}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.time_setting`]}</div>
|
||||||
|
{
|
||||||
|
showTimeField && (
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<div className={styles.label}>{currentLocales[`datepicker.time_setting.time_field`]}</div>
|
||||||
|
<Select value={timeField} onChange={setTimeField} style={{ width: '100%' }}>
|
||||||
|
{timeFields.map(item => (
|
||||||
|
<Select.Option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{showTimeInterval && (
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
{currentLocales[`datepicker.time_setting.time_interval`]}
|
||||||
|
<div className={styles.auto}>
|
||||||
|
<Switch size="small" checked={isAuto} onChange={(checked) => {
|
||||||
|
setIsAuto(checked)
|
||||||
|
if (checked) {
|
||||||
|
timeIntervalCache.current = timeInterval;
|
||||||
|
setTimeInterval()
|
||||||
|
} else {
|
||||||
|
setTimeInterval(timeIntervalCache.current || props.timeInterval || '10s')
|
||||||
|
}
|
||||||
|
}}/> {currentLocales[`datepicker.time_setting.time_interval.auto`]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.form}>
|
||||||
|
{
|
||||||
|
!isAuto && timeIntervalObject && (
|
||||||
|
<>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
value={timeIntervalObject.value}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
step={1}
|
||||||
|
precision={0}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
setTimeInterval(`${value}${timeIntervalObject.unit}`)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select value={timeIntervalObject.unit} onChange={(value) => setTimeInterval(`${timeIntervalObject.value}${value}`)} style={{ width: '100%' }}>
|
||||||
|
{timeIntervals.map((item) => (
|
||||||
|
<Select.Option key={item.value} value={item.value}>
|
||||||
|
{currentLocales[`datepicker.time_setting.time_interval.${item.value}`]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.apply}>
|
||||||
|
<Apply currentLocales={currentLocales} onApply={handleApply} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeSetting;
|
|
@ -0,0 +1,45 @@
|
||||||
|
.timeSetting {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formItem {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-switch {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import Apply from './Apply';
|
||||||
|
import styles from './TimeZone.less';
|
||||||
|
import { TIMEZONES } from './utils/time_zones';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { getDateString } from './utils/utils';
|
||||||
|
|
||||||
|
const TimeZone = props => {
|
||||||
|
const { currentLocales, onTimeZoneChange, onCancel, dateFormat } = props;
|
||||||
|
|
||||||
|
const [timeZone, setTimeZone] = useState();
|
||||||
|
const [searchValue, setSearchValue] = useState();
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
const index = TIMEZONES.findIndex(item => item.value === timeZone);
|
||||||
|
if (index !== -1) {
|
||||||
|
onTimeZoneChange(timeZone);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterList = useMemo(() => {
|
||||||
|
if (!searchValue) return TIMEZONES;
|
||||||
|
return TIMEZONES.filter(
|
||||||
|
item =>
|
||||||
|
item.label?.toLocaleLowerCase().indexOf(searchValue.toLocaleLowerCase()) !== -1
|
||||||
|
);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
const current = useMemo(() => {
|
||||||
|
return TIMEZONES.find(item => item.value === props.timeZone);
|
||||||
|
}, [props.timeZone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.timeZone}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.time_zone`]}</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Search by country, city, time zone, or GMT offset"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={e => setSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{filterList.map(item => (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={`${styles.item} ${timeZone === item.value ? styles.selected : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setTimeZone(item.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.name} title={item.label}>{`${item.label}`}</div>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{getDateString(undefined, item.value, dateFormat)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{current && (
|
||||||
|
<div className={styles.current}>
|
||||||
|
<div className={styles.title}>{currentLocales[`datepicker.time_zone.current`]}</div>
|
||||||
|
<div className={styles.value} title={current.label}>{`${current.label}`}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.apply}>
|
||||||
|
<Apply currentLocales={currentLocales} onApply={handleApply} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeZone;
|
|
@ -0,0 +1,67 @@
|
||||||
|
.timeZone {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px 8px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
height: calc(100% - 30px - 32px - 32px - 16px);
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 8px 0;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: calc(100% - 168px);
|
||||||
|
}
|
||||||
|
.date {
|
||||||
|
width: 160px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
& > .title {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
& > .value {
|
||||||
|
color: #101010;
|
||||||
|
width: calc(100% - 160px);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
import React, { useMemo, useState, useRef } from "react";
|
||||||
|
import { Button, Icon } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
|
import dateMath from "@elastic/datemath";
|
||||||
|
|
||||||
|
import Range from "./Range";
|
||||||
|
import locales from "./locales";
|
||||||
|
import isRelativeToNow from "./utils/pretty_duration";
|
||||||
|
import { toMilliseconds, fromMilliseconds } from "./utils/utils";
|
||||||
|
|
||||||
|
import styles from "./index.less";
|
||||||
|
|
||||||
|
const DEFAULT_COMMONLY_USED_RANGES = [
|
||||||
|
{
|
||||||
|
start: "now/d",
|
||||||
|
end: "now/d",
|
||||||
|
label: "Today",
|
||||||
|
key: "today",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now/w",
|
||||||
|
end: "now/w",
|
||||||
|
label: "This week",
|
||||||
|
key: "this_week",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-15m",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 15 minutes",
|
||||||
|
key: "last_15_minutes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-30m",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 30 minutes",
|
||||||
|
key: "last_30_minutes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-1h",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 1 hour",
|
||||||
|
key: "last_1_hour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-24h",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 24 hours",
|
||||||
|
key: "last_24_hours",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-7d",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 7 days",
|
||||||
|
key: "last_7_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-30d",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 30 days",
|
||||||
|
key: "last_30_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "now-90d",
|
||||||
|
end: "now",
|
||||||
|
label: "Last 90 days",
|
||||||
|
key: "last_90_days",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_RECENTLY_USED_RANGES_KEY = "recently-used-ranges";
|
||||||
|
|
||||||
|
const DatePicker = (props) => {
|
||||||
|
const {
|
||||||
|
locale = "en-US",
|
||||||
|
className = "",
|
||||||
|
popoverPlacement = "bottomLeft",
|
||||||
|
dateFormat = "YYYY-MM-DD HH:mm:ss",
|
||||||
|
start = "now-15m",
|
||||||
|
end = "now",
|
||||||
|
onRangeChange,
|
||||||
|
isRefreshPaused = true,
|
||||||
|
refreshInterval = 10000,
|
||||||
|
showTimeSetting = false,
|
||||||
|
shouldTimeField = true,
|
||||||
|
showTimeField = false,
|
||||||
|
timeField,
|
||||||
|
timeFields = [],
|
||||||
|
showTimeInterval = false,
|
||||||
|
timeInterval,
|
||||||
|
autoFitLoading = false,
|
||||||
|
timeZone = "Asia/Shanghai",
|
||||||
|
commonlyUsedRanges = DEFAULT_COMMONLY_USED_RANGES,
|
||||||
|
recentlyUsedRangesKey,
|
||||||
|
onRefreshChange,
|
||||||
|
onRefresh,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [prevQuickSelect, setPrevQuickSelect] = useState();
|
||||||
|
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(() => {
|
||||||
|
if (!recentlyUsedRangesKey) return [];
|
||||||
|
const history = localStorage.getItem(
|
||||||
|
`${recentlyUsedRangesKey}-${DEFAULT_RECENTLY_USED_RANGES_KEY}`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const ranges = JSON.parse(history);
|
||||||
|
return Array.isArray(ranges) ? ranges : [];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRangeChange = ({ start, end, quickSelect, isAbsolute }) => {
|
||||||
|
onRangeChange({ start, end, timeZone });
|
||||||
|
if (isAbsolute && recentlyUsedRangesKey) {
|
||||||
|
const newRecentlyUsedRanges = [...recentlyUsedRanges];
|
||||||
|
newRecentlyUsedRanges.unshift({ start, end, timeZone });
|
||||||
|
const last20 = newRecentlyUsedRanges.slice(0, 20);
|
||||||
|
setRecentlyUsedRanges(last20);
|
||||||
|
localStorage.setItem(
|
||||||
|
`${recentlyUsedRangesKey}-${DEFAULT_RECENTLY_USED_RANGES_KEY}`,
|
||||||
|
JSON.stringify(last20)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (quickSelect) {
|
||||||
|
setPrevQuickSelect(quickSelect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBounds = () => {
|
||||||
|
const startMoment = dateMath.parse(start);
|
||||||
|
const endMoment = dateMath.parse(end, { roundUp: true });
|
||||||
|
return {
|
||||||
|
min:
|
||||||
|
startMoment && startMoment.isValid()
|
||||||
|
? startMoment
|
||||||
|
: moment().subtract(15, "minute"),
|
||||||
|
max: endMoment && endMoment.isValid() ? endMoment : moment(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepBackward = () => {
|
||||||
|
const { min, max } = getBounds();
|
||||||
|
const diff = max.diff(min);
|
||||||
|
handleRangeChange({
|
||||||
|
start: moment(min)
|
||||||
|
.subtract(diff + 1, "ms")
|
||||||
|
.tz(timeZone)
|
||||||
|
.toISOString(),
|
||||||
|
end: moment(min)
|
||||||
|
.subtract(1, "ms")
|
||||||
|
.tz(timeZone)
|
||||||
|
.toISOString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepForward = () => {
|
||||||
|
const { min, max } = getBounds();
|
||||||
|
const diff = max.diff(min);
|
||||||
|
const endMoment = moment(max)
|
||||||
|
.add(diff + 1, "ms")
|
||||||
|
.tz(timeZone);
|
||||||
|
if (endMoment.diff(moment().tz(timeZone), "s") > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleRangeChange({
|
||||||
|
start: moment(max)
|
||||||
|
.add(1, "ms")
|
||||||
|
.tz(timeZone)
|
||||||
|
.toISOString(),
|
||||||
|
end: endMoment.toISOString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMinimum = useMemo(() => {
|
||||||
|
return showTimeSetting && showTimeField && shouldTimeField
|
||||||
|
? !timeField
|
||||||
|
: false;
|
||||||
|
}, [showTimeSetting, shouldTimeField, shouldTimeField, timeField]);
|
||||||
|
|
||||||
|
const isNextDisabled = useMemo(() => {
|
||||||
|
return isRelativeToNow(start, end);
|
||||||
|
}, [start, end]);
|
||||||
|
|
||||||
|
const RangeRef = React.createRef();
|
||||||
|
const [time, setTime] = useState(() => fromMilliseconds(refreshInterval));
|
||||||
|
const { value, units } = time;
|
||||||
|
|
||||||
|
const onPlayClick = () => {
|
||||||
|
if (RangeRef.current) {
|
||||||
|
const handleRefreshChange = RangeRef.current.handleRefreshChange;
|
||||||
|
if (handleRefreshChange) {
|
||||||
|
handleRefreshChange({
|
||||||
|
refreshInterval: toMilliseconds(units, value),
|
||||||
|
isRefreshPaused: !isRefreshPaused,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [reloadLoading, setReloadLoading] = useState(false)
|
||||||
|
const onRefreshClick = () => {
|
||||||
|
setReloadLoading(true)
|
||||||
|
onRefresh && onRefresh({ start, end });
|
||||||
|
setTimeout(()=>{
|
||||||
|
setReloadLoading(false)
|
||||||
|
}, 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.datePicker} ${
|
||||||
|
isMinimum ? styles.minimum : ""
|
||||||
|
} ${className}`}
|
||||||
|
>
|
||||||
|
<Button.Group className={styles.RangeBox}>
|
||||||
|
{!isMinimum && (
|
||||||
|
<Button
|
||||||
|
className={`${styles.iconBtn} common-ui-datepicker-backward`}
|
||||||
|
icon="left"
|
||||||
|
onClick={stepBackward}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Range
|
||||||
|
{...props}
|
||||||
|
onRef={RangeRef}
|
||||||
|
popoverPlacement={popoverPlacement}
|
||||||
|
dateFormat={dateFormat}
|
||||||
|
start={start}
|
||||||
|
end={end}
|
||||||
|
onRangeChange={handleRangeChange}
|
||||||
|
isRefreshPaused={isRefreshPaused}
|
||||||
|
refreshInterval={refreshInterval}
|
||||||
|
showTimeSetting={showTimeSetting}
|
||||||
|
timeFields={timeFields}
|
||||||
|
showTimeInterval={showTimeInterval}
|
||||||
|
timeInterval={timeInterval}
|
||||||
|
autoFitLoading={autoFitLoading}
|
||||||
|
timeZone={timeZone}
|
||||||
|
commonlyUsedRanges={commonlyUsedRanges}
|
||||||
|
recentlyUsedRanges={recentlyUsedRanges}
|
||||||
|
isMinimum={isMinimum}
|
||||||
|
prevQuickSelect={prevQuickSelect}
|
||||||
|
currentLocales={locales[locale] || {}}
|
||||||
|
onRefreshChange={onRefreshChange}
|
||||||
|
/>
|
||||||
|
{!isMinimum && (
|
||||||
|
<Button
|
||||||
|
disabled={isNextDisabled}
|
||||||
|
className={`${styles.iconBtn} common-ui-datepicker-Forward`}
|
||||||
|
icon="right"
|
||||||
|
onClick={stepForward}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button.Group>
|
||||||
|
<Button.Group className={styles.refreshBtn}>
|
||||||
|
<Button className={styles.play} onClick={onPlayClick}>
|
||||||
|
{isRefreshPaused ? (
|
||||||
|
<Icon type="caret-right" />
|
||||||
|
) : (
|
||||||
|
<Icon type="pause" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{onRefresh ? (
|
||||||
|
<Button className={styles.play} onClick={onRefreshClick}>
|
||||||
|
{reloadLoading ? (
|
||||||
|
<Icon type="loading" />
|
||||||
|
) : (
|
||||||
|
<Icon type="reload" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePicker;
|
|
@ -0,0 +1,52 @@
|
||||||
|
.datePicker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.RangeBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:global {
|
||||||
|
.ant-btn-group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.ant-btn {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-btn[disabled],
|
||||||
|
.ant-btn[disabled]:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBtn {
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.anticon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshBtn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 4px !important;
|
||||||
|
.play {
|
||||||
|
min-width: 30px;
|
||||||
|
max-width: 30px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### DatePicker
|
||||||
|
|
||||||
|
| Property | Description | Type | Default | Version |
|
||||||
|
| -------- | ----------- | ---- | ------- | ------- |
|
||||||
|
| className | 组件根元素的类名称 | string | - | 1.0.0 |
|
||||||
|
| popoverClassName | 弹窗元素的类名称 | string | - | 1.0.0 |
|
||||||
|
| popoverPlacement | 弹窗位置,可选 top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom | string | 'bottom' | 1.0.0 |
|
||||||
|
| locale | 语言 | string | 'en-US' | 1.0.0 |
|
||||||
|
| dateFormat | 日期格式化 | string | 'YYYY-MM-DD HH:mm:ss' | 1.0.0 |
|
||||||
|
| Start | 开始日期 | string | 'now-15m' | 1.0.0 |
|
||||||
|
| End | 结束日期 | string | 'now' | 1.0.0 |
|
||||||
|
| onRangeChange | 时间范围变更的回调 | ({start: string, end: string, isAbsolute?: boolean}) => void | - | 1.0.0 |
|
||||||
|
| isRefreshPaused | 是否暂停自动刷新 | boolean | true | 1.0.0 |
|
||||||
|
| refreshInterval | 自动刷新间隔 | number | 10000 | 1.0.0 |
|
||||||
|
| onRefreshChange | 自动刷新变更的回调 | ({isRefreshPaused: boolean, refreshInterval: number}) => void | - | 1.0.0 |
|
||||||
|
| onRefresh | 自动刷新触发的操作 | ({start: string, end: string, refreshInterval: number}) => void | - | 1.0.0 |
|
||||||
|
| showTimeSetting | 是否显示时间配置 | boolean | false | 1.0.0 |
|
||||||
|
| shouldTimeField | 当显示时间配置时,是否必须设置时间字段 | boolean | true | 1.0.0 |
|
||||||
|
| showTimeField | 是否显示时间字段配置 | boolean | false | 1.0.0 |
|
||||||
|
| timeField | 时间字段 | string | - | 1.0.0 |
|
||||||
|
| timeFields | 时间字段列表 | string[] | [] | 1.0.0 |
|
||||||
|
| showTimeInterval | 是否显示时间间隔 | boolean | false | 1.0.0 |
|
||||||
|
| timeInterval | 时间间隔 | string | - | 1.0.0 |
|
||||||
|
| onTimeSettingChange | 时间配置变更的回调 | ({timeField: string, timeInterval: string}) => void | - | 1.0.0 |
|
||||||
|
| autoFitLoading | 自动适配时间载入状态 | boolean | false | 1.0.0 |
|
||||||
|
| onAutoFit | 自动适配时间的回调 | () => void | - | 1.0.0 |
|
||||||
|
| timeZone | 时区 | string | 'Asia/Shanghai' | 1.0.0 |
|
||||||
|
| onTimeZoneChange | 时区变更的回调 | (timeZone: string) => void | - | 1.0.0 |
|
||||||
|
| commonlyUsedRanges | 快速选择列表 | {start: string, end: string, label: string}[] | [] | 1.0.0 |
|
||||||
|
| recentlyUsedRangesKey | 时间范围历史字段 | string | - | 1.0.0 |
|
|
@ -0,0 +1,44 @@
|
||||||
|
export default {
|
||||||
|
"datepicker.quick_select.auto_fit": "Auto fit",
|
||||||
|
"datepicker.quick_select.today": "Today",
|
||||||
|
"datepicker.quick_select.this_week": "This week",
|
||||||
|
"datepicker.quick_select.last_15_minutes": "Last 15 minutes",
|
||||||
|
"datepicker.quick_select.last_30_minutes": "Last 30 minutes",
|
||||||
|
"datepicker.quick_select.last_1_hour": "Last 1 hour",
|
||||||
|
"datepicker.quick_select.last_24_hours": "Last 24 hours",
|
||||||
|
"datepicker.quick_select.last_7_days": "Last 7 days",
|
||||||
|
"datepicker.quick_select.last_30_days": "Last 30 days",
|
||||||
|
"datepicker.quick_select.last_90_days": "Last 90 days",
|
||||||
|
"datepicker.quick_select": "Quick select",
|
||||||
|
"datepicker.quick_select.last": "Last",
|
||||||
|
"datepicker.quick_select.next": "Next",
|
||||||
|
"datepicker.start_and_end_times": "Start and end times",
|
||||||
|
"datepicker.start_and_end_times.start_time": "Start time",
|
||||||
|
"datepicker.start_and_end_times.end_time": "End time",
|
||||||
|
"datepicker.start_and_end_times.error": "End time must be after start time",
|
||||||
|
"datepicker.start_and_end_times.recent": "Recent",
|
||||||
|
"datepicker.time_setting": "Time setting",
|
||||||
|
"datepicker.time_setting.time_field": "Time field",
|
||||||
|
"datepicker.time_setting.time_interval": "Time interval",
|
||||||
|
"datepicker.time_setting.time_interval.auto": "Auto",
|
||||||
|
"datepicker.time_setting.time_interval.ms": "Millisecond",
|
||||||
|
"datepicker.time_setting.time_interval.s": "Second",
|
||||||
|
"datepicker.time_setting.time_interval.m": "Minute",
|
||||||
|
"datepicker.time_setting.time_interval.h": "Hour",
|
||||||
|
"datepicker.time_setting.time_interval.d": "Day",
|
||||||
|
"datepicker.time_setting.time_interval.w": "Week",
|
||||||
|
"datepicker.time_setting.time_interval.M": "Month",
|
||||||
|
"datepicker.time_setting.time_interval.y": "Year",
|
||||||
|
"datepicker.time_zone": "Time zone",
|
||||||
|
"datepicker.time_zone.current": "Current time zone",
|
||||||
|
"datepicker.refresh_every": "Refresh every",
|
||||||
|
"datepicker.time.units.s": "seconds",
|
||||||
|
"datepicker.time.units.m": "minutes",
|
||||||
|
"datepicker.time.units.h": "hours",
|
||||||
|
"datepicker.time.units.d": "days",
|
||||||
|
"datepicker.time.units.w": "weeks",
|
||||||
|
"datepicker.time.units.M": "months",
|
||||||
|
"datepicker.time.units.y": "years",
|
||||||
|
"datepicker.cancel": "Cancel",
|
||||||
|
"datepicker.apply": "Apply",
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import en_us from './en-US';
|
||||||
|
import zh_cn from './zh-CN';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'en-US': en_us,
|
||||||
|
'zh-CN': zh_cn
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
export default {
|
||||||
|
"datepicker.quick_select.auto_fit": "自动适配",
|
||||||
|
"datepicker.quick_select.today": "今天",
|
||||||
|
"datepicker.quick_select.this_week": "这个星期",
|
||||||
|
"datepicker.quick_select.last_15_minutes": "最近15分钟",
|
||||||
|
"datepicker.quick_select.last_30_minutes": "最近30分钟",
|
||||||
|
"datepicker.quick_select.last_1_hour": "最近1小时",
|
||||||
|
"datepicker.quick_select.last_24_hours": "最近24小时",
|
||||||
|
"datepicker.quick_select.last_7_days": "最近7天",
|
||||||
|
"datepicker.quick_select.last_30_days": "最近30天",
|
||||||
|
"datepicker.quick_select.last_90_days": "最近90天",
|
||||||
|
"datepicker.quick_select": "快速选择",
|
||||||
|
"datepicker.quick_select.last": "最近",
|
||||||
|
"datepicker.quick_select.next": "未来",
|
||||||
|
"datepicker.start_and_end_times": "时间范围",
|
||||||
|
"datepicker.start_and_end_times.start_time": "开始时间",
|
||||||
|
"datepicker.start_and_end_times.end_time": "结束时间",
|
||||||
|
"datepicker.start_and_end_times.error": "结束时间必须在开始时间之后",
|
||||||
|
"datepicker.start_and_end_times.recent": "最近使用的",
|
||||||
|
"datepicker.time_setting": "时间设置",
|
||||||
|
"datepicker.time_setting.time_field": "时间字段",
|
||||||
|
"datepicker.time_setting.time_interval": "时间间隔",
|
||||||
|
"datepicker.time_setting.time_interval.auto": "自动",
|
||||||
|
"datepicker.time_setting.time_interval.ms": "毫秒",
|
||||||
|
"datepicker.time_setting.time_interval.s": "秒",
|
||||||
|
"datepicker.time_setting.time_interval.m": "分",
|
||||||
|
"datepicker.time_setting.time_interval.h": "时",
|
||||||
|
"datepicker.time_setting.time_interval.d": "天",
|
||||||
|
"datepicker.time_setting.time_interval.w": "周",
|
||||||
|
"datepicker.time_setting.time_interval.M": "月",
|
||||||
|
"datepicker.time_setting.time_interval.y": "年",
|
||||||
|
"datepicker.time_zone": "时区",
|
||||||
|
"datepicker.time_zone.current": "当前时区",
|
||||||
|
"datepicker.refresh_every": "刷新间隔",
|
||||||
|
"datepicker.time.units.s": "秒",
|
||||||
|
"datepicker.time.units.m": "分",
|
||||||
|
"datepicker.time.units.h": "时",
|
||||||
|
"datepicker.time.units.d": "天",
|
||||||
|
"datepicker.time.units.w": "周",
|
||||||
|
"datepicker.time.units.M": "月",
|
||||||
|
"datepicker.time.units.y": "年",
|
||||||
|
"datepicker.cancel": "取消",
|
||||||
|
"datepicker.apply": "应用",
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
export class AsyncInterval {
|
||||||
|
timeoutId = null;
|
||||||
|
isStopped = false;
|
||||||
|
__pendingFn = () => {};
|
||||||
|
|
||||||
|
constructor(fn, refreshInterval) {
|
||||||
|
this.setAsyncInterval(fn, refreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAsyncInterval = (fn, milliseconds) => {
|
||||||
|
if (!this.isStopped) {
|
||||||
|
this.timeoutId = window.setTimeout(async () => {
|
||||||
|
this.__pendingFn = await fn();
|
||||||
|
this.setAsyncInterval(fn, milliseconds);
|
||||||
|
}, milliseconds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
stop = () => {
|
||||||
|
this.isStopped = true;
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
window.clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import dateMath from '@elastic/datemath';
|
||||||
|
import { parseRelativeParts, toRelativeStringFromParts } from './relative_utils';
|
||||||
|
|
||||||
|
export const DATE_MODES = {
|
||||||
|
ABSOLUTE: 'absolute',
|
||||||
|
RELATIVE: 'relative',
|
||||||
|
NOW: 'now',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDateMode(value) {
|
||||||
|
if (value === 'now') {
|
||||||
|
return DATE_MODES.NOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('now')) {
|
||||||
|
return DATE_MODES.RELATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DATE_MODES.ABSOLUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toAbsoluteString(value, roundUp = false) {
|
||||||
|
const valueAsMoment = dateMath.parse(value, { roundUp });
|
||||||
|
if (!valueAsMoment) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return valueAsMoment.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRelativeString(value) {
|
||||||
|
return toRelativeStringFromParts(parseRelativeParts(value));
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import dateMath from '@elastic/datemath';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { timeUnits, timeUnitsPlural } from './time_units';
|
||||||
|
import { getDateMode, DATE_MODES } from './date_modes';
|
||||||
|
import { parseRelativeParts } from './relative_utils';
|
||||||
|
|
||||||
|
const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
|
||||||
|
|
||||||
|
export const commonDurationRanges = [
|
||||||
|
{ start: 'now/d', end: 'now/d', label: 'Today' },
|
||||||
|
{ start: 'now/w', end: 'now/w', label: 'This week' },
|
||||||
|
{ start: 'now/M', end: 'now/M', label: 'This month' },
|
||||||
|
{ start: 'now/y', end: 'now/y', label: 'This year' },
|
||||||
|
{ start: 'now-1d/d', end: 'now-1d/d', label: 'Yesterday' },
|
||||||
|
{ start: 'now/w', end: 'now', label: 'Week to date' },
|
||||||
|
{ start: 'now/M', end: 'now', label: 'Month to date' },
|
||||||
|
{ start: 'now/y', end: 'now', label: 'Year to date' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function cantLookup(timeFrom, timeTo, dateFormat) {
|
||||||
|
const displayFrom = formatTimeString(timeFrom, dateFormat);
|
||||||
|
const displayTo = formatTimeString(timeTo, dateFormat, true);
|
||||||
|
return `${displayFrom} ~ ${displayTo}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function isRelativeToNow(timeFrom, timeTo) {
|
||||||
|
const fromDateMode = getDateMode(timeFrom);
|
||||||
|
const toDateMode = getDateMode(timeTo);
|
||||||
|
const isLast = fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW;
|
||||||
|
const isNext = fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE;
|
||||||
|
const isRelative = fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.RELATIVE;
|
||||||
|
return isLast || isNext || isRelative;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimeString(timeString, dateFormat, roundUp = false, locale = 'en') {
|
||||||
|
const timeAsMoment = moment(timeString, ISO_FORMAT, true);
|
||||||
|
if (timeAsMoment.isValid()) {
|
||||||
|
return timeAsMoment.locale(locale).format(dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeString === 'now') {
|
||||||
|
return 'now';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryParse = dateMath.parse(timeString, { roundUp: roundUp });
|
||||||
|
if (moment.isMoment(tryParse)) {
|
||||||
|
return `~ ${tryParse.locale(locale).fromNow()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyDuration(timeFrom, timeTo, quickRanges = [], dateFormat, locales) {
|
||||||
|
const matchingQuickRange = quickRanges.find(({ start: quickFrom, end: quickTo }) => {
|
||||||
|
return timeFrom === quickFrom && timeTo === quickTo;
|
||||||
|
});
|
||||||
|
if (matchingQuickRange && matchingQuickRange.key) {
|
||||||
|
return locales[`datepicker.quick_select.${matchingQuickRange.key}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelativeToNow(timeFrom, timeTo)) {
|
||||||
|
let timeTense;
|
||||||
|
let relativeParts;
|
||||||
|
if (getDateMode(timeTo) === DATE_MODES.NOW) {
|
||||||
|
timeTense = locales[`datepicker.quick_select.last`];
|
||||||
|
relativeParts = parseRelativeParts(timeFrom);
|
||||||
|
} else {
|
||||||
|
timeTense = locales[`datepicker.quick_select.next`];
|
||||||
|
relativeParts = parseRelativeParts(timeTo);
|
||||||
|
}
|
||||||
|
const countTimeUnit = relativeParts.unit.substring(0, 1);
|
||||||
|
let countTimeUnitFullName = locales[`datepicker.time.units.${countTimeUnit}`]
|
||||||
|
if (relativeParts.count === 1 && countTimeUnitFullName.substring(countTimeUnitFullName.length - 1) === 's') {
|
||||||
|
countTimeUnitFullName = countTimeUnitFullName.substring(0, countTimeUnitFullName.length - 1)
|
||||||
|
}
|
||||||
|
let text = `${timeTense} ${relativeParts.count} ${countTimeUnitFullName}`;
|
||||||
|
if (relativeParts.round && relativeParts.roundUnit) {
|
||||||
|
text += ` rounded to the ${timeUnits[relativeParts.roundUnit]}`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cantLookup(timeFrom, timeTo, dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showPrettyDuration(timeFrom, timeTo, quickRanges = []) {
|
||||||
|
const matchingQuickRange = quickRanges.find(({ start: quickFrom, end: quickTo }) => {
|
||||||
|
return timeFrom === quickFrom && timeTo === quickTo;
|
||||||
|
});
|
||||||
|
if (matchingQuickRange) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRelativeToNow(timeFrom, timeTo);
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import dateMath from '@elastic/datemath';
|
||||||
|
import { relativeUnitsFromLargestToSmallest } from './relative_options';
|
||||||
|
import { DATE_MODES } from './date_modes';
|
||||||
|
import _isString from 'lodash/isString';
|
||||||
|
|
||||||
|
const LAST = 'last';
|
||||||
|
const NEXT = 'next';
|
||||||
|
|
||||||
|
const isNow = value => value === DATE_MODES.NOW;
|
||||||
|
|
||||||
|
export const parseTimeParts = (start, end) => {
|
||||||
|
const results = {
|
||||||
|
timeTense: LAST,
|
||||||
|
timeUnits: 'm',
|
||||||
|
timeValue: 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = isNow(start) ? end : start;
|
||||||
|
|
||||||
|
const matches = _isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operator = matches[2];
|
||||||
|
const matchedTimeValue = matches[3];
|
||||||
|
const timeUnits = matches[4];
|
||||||
|
|
||||||
|
if (matchedTimeValue && timeUnits && operator) {
|
||||||
|
return {
|
||||||
|
timeTense: operator === '+' ? NEXT : LAST,
|
||||||
|
timeUnits,
|
||||||
|
timeValue: parseInt(matchedTimeValue, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = moment.duration(moment().diff(dateMath.parse(value)));
|
||||||
|
let unitOp = '';
|
||||||
|
for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) {
|
||||||
|
const as = duration.as(relativeUnitsFromLargestToSmallest[i]);
|
||||||
|
if (as < 0) {
|
||||||
|
unitOp = '+';
|
||||||
|
}
|
||||||
|
if (Math.abs(as) > 1) {
|
||||||
|
return {
|
||||||
|
timeValue: Math.round(Math.abs(as)),
|
||||||
|
timeUnits: relativeUnitsFromLargestToSmallest[i],
|
||||||
|
timeTense: unitOp === '+' ? NEXT : LAST,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
export const relativeOptions = [
|
||||||
|
{ text: 'Seconds ago', value: 's' },
|
||||||
|
{ text: 'Minutes ago', value: 'm' },
|
||||||
|
{ text: 'Hours ago', value: 'h' },
|
||||||
|
{ text: 'Days ago', value: 'd' },
|
||||||
|
{ text: 'Weeks ago', value: 'w' },
|
||||||
|
{ text: 'Months ago', value: 'M' },
|
||||||
|
{ text: 'Years ago', value: 'y' },
|
||||||
|
|
||||||
|
{ text: 'Seconds from now', value: 's+' },
|
||||||
|
{ text: 'Minutes from now', value: 'm+' },
|
||||||
|
{ text: 'Hours from now', value: 'h+' },
|
||||||
|
{ text: 'Days from now', value: 'd+' },
|
||||||
|
{ text: 'Weeks from now', value: 'w+' },
|
||||||
|
{ text: 'Months from now', value: 'M+' },
|
||||||
|
{ text: 'Years from now', value: 'y+' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const timeUnitIds = relativeOptions.map(({ value }) => value).filter(value => !value.includes('+'));
|
||||||
|
|
||||||
|
export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse();
|
|
@ -0,0 +1,59 @@
|
||||||
|
import dateMath from '@elastic/datemath';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { relativeUnitsFromLargestToSmallest } from './relative_options';
|
||||||
|
import _get from 'lodash/get';
|
||||||
|
import _isString from 'lodash/isString';
|
||||||
|
|
||||||
|
const ROUND_DELIMETER = '/';
|
||||||
|
|
||||||
|
export function parseRelativeParts(value) {
|
||||||
|
const matches = _isString(value) && value.match(/now(([-+])([0-9]+)([smhdwMy])(\/[smhdwMy])?)?/);
|
||||||
|
|
||||||
|
const operator = matches && matches[2];
|
||||||
|
const count = matches && matches[3];
|
||||||
|
const unit = matches && matches[4];
|
||||||
|
const roundBy = matches && matches[5];
|
||||||
|
|
||||||
|
if (count && unit) {
|
||||||
|
const isRounded = roundBy ? true : false;
|
||||||
|
const roundUnit = isRounded && roundBy ? roundBy.replace(ROUND_DELIMETER, '') : undefined;
|
||||||
|
return {
|
||||||
|
count: parseInt(count, 10),
|
||||||
|
unit: operator === '+' ? `${unit}+` : unit,
|
||||||
|
round: isRounded,
|
||||||
|
...(roundUnit ? { roundUnit } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = { count: 0, unit: 's', round: false };
|
||||||
|
const duration = moment.duration(moment().diff(dateMath.parse(value)));
|
||||||
|
let unitOp = '';
|
||||||
|
for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) {
|
||||||
|
const asRelative = duration.as(relativeUnitsFromLargestToSmallest[i]);
|
||||||
|
if (asRelative < 0) unitOp = '+';
|
||||||
|
if (Math.abs(asRelative) > 1) {
|
||||||
|
results.count = Math.round(Math.abs(asRelative));
|
||||||
|
results.unit = relativeUnitsFromLargestToSmallest[i] + unitOp;
|
||||||
|
results.round = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toRelativeStringFromParts = relativeParts => {
|
||||||
|
const count = _get(relativeParts, 'count', 0);
|
||||||
|
const isRounded = _get(relativeParts, 'round', false);
|
||||||
|
|
||||||
|
if (count === 0 && !isRounded) {
|
||||||
|
return 'now';
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = _get(relativeParts, 'unit', 's').match(/([smhdwMy])(\+)?/);
|
||||||
|
const unit = matches[1];
|
||||||
|
const operator = matches && matches[2] ? matches[2] : '-';
|
||||||
|
const round = isRounded ? `${ROUND_DELIMETER}${unit}` : '';
|
||||||
|
|
||||||
|
return `now${operator}${count}${unit}${round}`;
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
export const timeUnits = {
|
||||||
|
s: 'second',
|
||||||
|
m: 'minute',
|
||||||
|
h: 'hour',
|
||||||
|
d: 'day',
|
||||||
|
w: 'week',
|
||||||
|
M: 'month',
|
||||||
|
y: 'year',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const timeUnitsPlural = {
|
||||||
|
s: 'seconds',
|
||||||
|
m: 'minutes',
|
||||||
|
h: 'hours',
|
||||||
|
d: 'days',
|
||||||
|
w: 'weeks',
|
||||||
|
M: 'months',
|
||||||
|
y: 'years',
|
||||||
|
};
|
|
@ -0,0 +1,556 @@
|
||||||
|
export const TIMEZONES = [
|
||||||
|
{
|
||||||
|
value: 'Pacific/Midway',
|
||||||
|
label: '(GMT-11:00) Midway Island, Samoa (SST)',
|
||||||
|
offset: -11,
|
||||||
|
abbrev: 'SST',
|
||||||
|
altName: 'Samoa Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pacific/Honolulu',
|
||||||
|
label: '(GMT-10:00) Hawaii (HAST)',
|
||||||
|
offset: -10,
|
||||||
|
abbrev: 'HAST',
|
||||||
|
altName: 'Hawaii-Aleutian Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Juneau',
|
||||||
|
label: '(GMT-8:00) Alaska (AKDT)',
|
||||||
|
offset: -8,
|
||||||
|
abbrev: 'AKDT',
|
||||||
|
altName: 'Alaska Daylight Time',
|
||||||
|
},
|
||||||
|
{ value: 'America/Dawson', label: '(GMT-7:00) Dawson, Yukon ', offset: -7 },
|
||||||
|
{
|
||||||
|
value: 'America/Phoenix',
|
||||||
|
label: '(GMT-7:00) Arizona (MST)',
|
||||||
|
offset: -7,
|
||||||
|
abbrev: 'MST',
|
||||||
|
altName: 'Mountain Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Tijuana',
|
||||||
|
label: '(GMT-7:00) Tijuana (PDT)',
|
||||||
|
offset: -7,
|
||||||
|
abbrev: 'PDT',
|
||||||
|
altName: 'Pacific Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Los_Angeles',
|
||||||
|
label: '(GMT-7:00) Pacific Time (PDT)',
|
||||||
|
offset: -7,
|
||||||
|
abbrev: 'PDT',
|
||||||
|
altName: 'Pacific Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Boise',
|
||||||
|
label: '(GMT-6:00) Mountain Time (MDT)',
|
||||||
|
offset: -6,
|
||||||
|
abbrev: 'MDT',
|
||||||
|
altName: 'Mountain Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Chihuahua',
|
||||||
|
label: '(GMT-6:00) Chihuahua, La Paz, Mazatlan ',
|
||||||
|
offset: -6,
|
||||||
|
abbrev: 'HEPMX',
|
||||||
|
altName: 'Mexican Pacific Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Regina',
|
||||||
|
label: '(GMT-6:00) Saskatchewan (CST)',
|
||||||
|
offset: -6,
|
||||||
|
abbrev: 'CST',
|
||||||
|
altName: 'Central Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Belize',
|
||||||
|
label: '(GMT-6:00) Central America (CST)',
|
||||||
|
offset: -6,
|
||||||
|
abbrev: 'CST',
|
||||||
|
altName: 'Central Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Chicago',
|
||||||
|
label: '(GMT-5:00) Central Time (CDT)',
|
||||||
|
offset: -5,
|
||||||
|
abbrev: 'CDT',
|
||||||
|
altName: 'Central Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Mexico_City',
|
||||||
|
label: '(GMT-5:00) Guadalajara, Mexico City, Monterrey (CDT)',
|
||||||
|
offset: -5,
|
||||||
|
abbrev: 'CDT',
|
||||||
|
altName: 'Central Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Bogota',
|
||||||
|
label: '(GMT-5:00) Bogota, Lima, Quito (COT)',
|
||||||
|
offset: -5,
|
||||||
|
abbrev: 'COT',
|
||||||
|
altName: 'Colombia Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Lima',
|
||||||
|
label: '(GMT-5:00) Pittsburgh (PET)',
|
||||||
|
offset: -5,
|
||||||
|
abbrev: 'PET',
|
||||||
|
altName: 'Peru Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Detroit',
|
||||||
|
label: '(GMT-4:00) Eastern Time (EDT)',
|
||||||
|
offset: -4,
|
||||||
|
abbrev: 'EDT',
|
||||||
|
altName: 'Eastern Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Caracas',
|
||||||
|
label: '(GMT-4:00) Caracas, La Paz (VET)',
|
||||||
|
offset: -4,
|
||||||
|
abbrev: 'VET',
|
||||||
|
altName: 'Venezuela Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Santiago',
|
||||||
|
label: '(GMT-4:00) Santiago (CLT)',
|
||||||
|
offset: -4,
|
||||||
|
abbrev: 'CLT',
|
||||||
|
altName: 'Chile Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Sao_Paulo',
|
||||||
|
label: '(GMT-3:00) Brasilia (BRT)',
|
||||||
|
offset: -3,
|
||||||
|
abbrev: 'BRT',
|
||||||
|
altName: 'Brasilia Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Montevideo',
|
||||||
|
label: '(GMT-3:00) Montevideo (UYT)',
|
||||||
|
offset: -3,
|
||||||
|
abbrev: 'UYT',
|
||||||
|
altName: 'Uruguay Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/Argentina/Buenos_Aires',
|
||||||
|
label: '(GMT-3:00) Buenos Aires, Georgetown ',
|
||||||
|
offset: -3,
|
||||||
|
abbrev: 'America/Argentina/Buenos_Aires',
|
||||||
|
altName: 'America/Argentina/Buenos_Aires',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'America/St_Johns',
|
||||||
|
label: '(GMT-2:30) Newfoundland and Labrador (HETN)',
|
||||||
|
offset: -2.5,
|
||||||
|
abbrev: 'HETN',
|
||||||
|
altName: 'Newfoundland Daylight Time',
|
||||||
|
},
|
||||||
|
{ value: 'America/Godthab', label: '(GMT-2:00) Greenland ', offset: -2 },
|
||||||
|
{
|
||||||
|
value: 'Atlantic/Cape_Verde',
|
||||||
|
label: '(GMT-1:00) Cape Verde Islands (CVT)',
|
||||||
|
offset: -1,
|
||||||
|
abbrev: 'CVT',
|
||||||
|
altName: 'Cape Verde Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Atlantic/Azores',
|
||||||
|
label: '(GMT+0:00) Azores ',
|
||||||
|
offset: 0,
|
||||||
|
abbrev: 'AZOST',
|
||||||
|
altName: 'Azores Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Etc/GMT',
|
||||||
|
label: '(GMT+0:00) UTC (GMT)',
|
||||||
|
offset: 0,
|
||||||
|
abbrev: 'GMT',
|
||||||
|
altName: 'ETC/GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Africa/Casablanca',
|
||||||
|
label: '(GMT+0:00) Casablanca, Monrovia (WET)',
|
||||||
|
offset: 0,
|
||||||
|
abbrev: 'WET',
|
||||||
|
altName: 'Western European Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/London',
|
||||||
|
label: '(GMT+1:00) Edinburgh, London (BST)',
|
||||||
|
offset: 1,
|
||||||
|
abbrev: 'BST',
|
||||||
|
altName: 'British Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Dublin',
|
||||||
|
label: '(GMT+1:00) Dublin (BST)',
|
||||||
|
offset: 1,
|
||||||
|
abbrev: 'BST',
|
||||||
|
altName: 'British Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Lisbon',
|
||||||
|
label: '(GMT+1:00) Lisbon (WEST)',
|
||||||
|
offset: 1,
|
||||||
|
abbrev: 'WEST',
|
||||||
|
altName: 'Western European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Atlantic/Canary',
|
||||||
|
label: '(GMT+1:00) Canary Islands (WEST)',
|
||||||
|
offset: 1,
|
||||||
|
abbrev: 'WEST',
|
||||||
|
altName: 'Western European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Africa/Algiers',
|
||||||
|
label: '(GMT+1:00) West Central Africa (CET)',
|
||||||
|
offset: 1,
|
||||||
|
abbrev: 'CET',
|
||||||
|
altName: 'Central European Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Belgrade',
|
||||||
|
label: '(GMT+2:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague (CEST)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CEST',
|
||||||
|
altName: 'Central European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Sarajevo',
|
||||||
|
label: '(GMT+2:00) Sarajevo, Skopje, Warsaw, Zagreb (CEST)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CEST',
|
||||||
|
altName: 'Central European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Brussels',
|
||||||
|
label: '(GMT+2:00) Brussels, Copenhagen, Madrid, Paris (CEST)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CEST',
|
||||||
|
altName: 'Central European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Amsterdam',
|
||||||
|
label: '(GMT+2:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna (CEST)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CEST',
|
||||||
|
altName: 'Central European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Africa/Cairo',
|
||||||
|
label: '(GMT+2:00) Cairo (EET)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'EET',
|
||||||
|
altName: 'Eastern European Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Africa/Harare',
|
||||||
|
label: '(GMT+2:00) Harare, Pretoria (CAT)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CAT',
|
||||||
|
altName: 'Central Africa Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Berlin',
|
||||||
|
label: '(GMT+2:00) Frankfurt (CEST)',
|
||||||
|
offset: 2,
|
||||||
|
abbrev: 'CEST',
|
||||||
|
altName: 'Central European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Bucharest',
|
||||||
|
label: '(GMT+3:00) Bucharest (EEST)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'EEST',
|
||||||
|
altName: 'Eastern European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Helsinki',
|
||||||
|
label: '(GMT+3:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius (EEST)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'EEST',
|
||||||
|
altName: 'Eastern European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Athens',
|
||||||
|
label: '(GMT+3:00) Athens, Minsk (EEST)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'EEST',
|
||||||
|
altName: 'Eastern European Summer Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Jerusalem',
|
||||||
|
label: '(GMT+3:00) Jerusalem ',
|
||||||
|
offset: 3,
|
||||||
|
altName: 'Israel Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Europe/Moscow',
|
||||||
|
label: '(GMT+3:00) Istanbul, Moscow, St. Petersburg, Volgograd (MSK)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'MSK',
|
||||||
|
altName: 'Moscow Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kuwait',
|
||||||
|
label: '(GMT+3:00) Kuwait, Riyadh (AST)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'AST',
|
||||||
|
altName: 'Arabian Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Africa/Nairobi',
|
||||||
|
label: '(GMT+3:00) Nairobi (EAT)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'EAT',
|
||||||
|
altName: 'East Africa Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Baghdad',
|
||||||
|
label: '(GMT+3:00) Baghdad (AST)',
|
||||||
|
offset: 3,
|
||||||
|
abbrev: 'AST',
|
||||||
|
altName: 'Arabian Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Dubai',
|
||||||
|
label: '(GMT+4:00) Abu Dhabi, Muscat (GST)',
|
||||||
|
offset: 4,
|
||||||
|
abbrev: 'GST',
|
||||||
|
altName: 'Gulf Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Baku',
|
||||||
|
label: '(GMT+4:00) Baku, Tbilisi, Yerevan (AZT)',
|
||||||
|
offset: 4,
|
||||||
|
abbrev: 'AZT',
|
||||||
|
altName: 'Azerbaijan Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Tehran',
|
||||||
|
label: '(GMT+4:30) Tehran (IRDT)',
|
||||||
|
offset: 4.5,
|
||||||
|
abbrev: 'IRDT',
|
||||||
|
altName: 'Iran Daylight Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kabul',
|
||||||
|
label: '(GMT+4:30) Kabul (AFT)',
|
||||||
|
offset: 4.5,
|
||||||
|
abbrev: 'AFT',
|
||||||
|
altName: 'Afghanistan Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Yekaterinburg',
|
||||||
|
label: '(GMT+5:00) Ekaterinburg (YEKT)',
|
||||||
|
offset: 5,
|
||||||
|
abbrev: 'YEKT',
|
||||||
|
altName: 'Yekaterinburg Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Karachi',
|
||||||
|
label: '(GMT+5:00) Islamabad, Karachi, Tashkent (PKT)',
|
||||||
|
offset: 5,
|
||||||
|
abbrev: 'PKT',
|
||||||
|
altName: 'Pakistan Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kolkata',
|
||||||
|
label: '(GMT+5:30) Chennai, Kolkata, Mumbai, New Delhi (IST)',
|
||||||
|
offset: 5.5,
|
||||||
|
abbrev: 'IST',
|
||||||
|
altName: 'India Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Colombo',
|
||||||
|
label: '(GMT+5:30) Sri Jayawardenepura (IST)',
|
||||||
|
offset: 5.5,
|
||||||
|
abbrev: 'IST',
|
||||||
|
altName: 'India Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kathmandu',
|
||||||
|
label: '(GMT+5:45) Kathmandu ',
|
||||||
|
offset: 5.75,
|
||||||
|
abbrev: 'UTC+5.75',
|
||||||
|
altName: 'Kathmandu Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Dhaka',
|
||||||
|
label: '(GMT+6:00) Astana, Dhaka (BST)',
|
||||||
|
offset: 6,
|
||||||
|
abbrev: 'BST',
|
||||||
|
altName: 'Bangladesh Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Almaty',
|
||||||
|
label: '(GMT+6:00) Almaty, Novosibirsk (ALMT)',
|
||||||
|
offset: 6,
|
||||||
|
abbrev: 'ALMT',
|
||||||
|
altName: 'East Kazakhstan Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Rangoon',
|
||||||
|
label: '(GMT+6:30) Yangon Rangoon ',
|
||||||
|
offset: 6.5,
|
||||||
|
abbrev: 'Asia/Yangon',
|
||||||
|
altName: 'Asia/Yangon',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Bangkok',
|
||||||
|
label: '(GMT+7:00) Bangkok, Hanoi, Jakarta (ICT)',
|
||||||
|
offset: 7,
|
||||||
|
abbrev: 'ICT',
|
||||||
|
altName: 'Indochina Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Krasnoyarsk',
|
||||||
|
label: '(GMT+7:00) Krasnoyarsk (KRAT)',
|
||||||
|
offset: 7,
|
||||||
|
abbrev: 'KRAT',
|
||||||
|
altName: 'Krasnoyarsk Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Shanghai',
|
||||||
|
label: '(GMT+8:00) Beijing, Chongqing, Hong Kong SAR, Urumqi (CST)',
|
||||||
|
offset: 8,
|
||||||
|
abbrev: 'CST',
|
||||||
|
altName: 'China Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kuala_Lumpur',
|
||||||
|
label: '(GMT+8:00) Kuala Lumpur, Singapore (MYT)',
|
||||||
|
offset: 8,
|
||||||
|
abbrev: 'MYT',
|
||||||
|
altName: 'Malaysia Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Taipei',
|
||||||
|
label: '(GMT+8:00) Taipei (CST)',
|
||||||
|
offset: 8,
|
||||||
|
abbrev: 'CST',
|
||||||
|
altName: 'Taipei Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Perth',
|
||||||
|
label: '(GMT+8:00) Perth (AWST)',
|
||||||
|
offset: 8,
|
||||||
|
abbrev: 'AWST',
|
||||||
|
altName: 'Australian Western Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Irkutsk',
|
||||||
|
label: '(GMT+8:00) Irkutsk, Ulaanbaatar (IRKT)',
|
||||||
|
offset: 8,
|
||||||
|
abbrev: 'IRKT',
|
||||||
|
altName: 'Irkutsk Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Seoul',
|
||||||
|
label: '(GMT+9:00) Seoul (KST)',
|
||||||
|
offset: 9,
|
||||||
|
abbrev: 'KST',
|
||||||
|
altName: 'Korean Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Tokyo',
|
||||||
|
label: '(GMT+9:00) Osaka, Sapporo, Tokyo (JST)',
|
||||||
|
offset: 9,
|
||||||
|
abbrev: 'JST',
|
||||||
|
altName: 'Japan Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Darwin',
|
||||||
|
label: '(GMT+9:30) Darwin (ACST)',
|
||||||
|
offset: 9.5,
|
||||||
|
abbrev: 'ACST',
|
||||||
|
altName: 'Australian Central Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Adelaide',
|
||||||
|
label: '(GMT+9:30) Adelaide (ACST)',
|
||||||
|
offset: 9.5,
|
||||||
|
abbrev: 'ACST',
|
||||||
|
altName: 'Australian Central Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Yakutsk',
|
||||||
|
label: '(GMT+10:00) Yakutsk (YAKT)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'YAKT',
|
||||||
|
altName: 'Yakutsk Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Sydney',
|
||||||
|
label: '(GMT+10:00) Canberra, Melbourne, Sydney (AEST)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'AEST',
|
||||||
|
altName: 'Australian Eastern Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Brisbane',
|
||||||
|
label: '(GMT+10:00) Brisbane (AEST)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'AEST',
|
||||||
|
altName: 'Australian Eastern Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Australia/Hobart',
|
||||||
|
label: '(GMT+10:00) Hobart (AEST)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'AEST',
|
||||||
|
altName: 'Australian Eastern Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Vladivostok',
|
||||||
|
label: '(GMT+10:00) Vladivostok (VLAT)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'VLAT',
|
||||||
|
altName: 'Vladivostok Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pacific/Guam',
|
||||||
|
label: '(GMT+10:00) Guam, Port Moresby (ChST)',
|
||||||
|
offset: 10,
|
||||||
|
abbrev: 'ChST',
|
||||||
|
altName: 'Chamorro Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Magadan',
|
||||||
|
label: '(GMT+11:00) Magadan, Solomon Islands, New Caledonia (MAGT)',
|
||||||
|
offset: 11,
|
||||||
|
abbrev: 'MAGT',
|
||||||
|
altName: 'Magadan Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Asia/Kamchatka',
|
||||||
|
label: '(GMT+12:00) Kamchatka, Marshall Islands (PETT)',
|
||||||
|
offset: 12,
|
||||||
|
abbrev: 'PETT',
|
||||||
|
altName: 'Petropavlovsk-Kamchatski Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pacific/Fiji',
|
||||||
|
label: '(GMT+12:00) Fiji Islands (FJT)',
|
||||||
|
offset: 12,
|
||||||
|
abbrev: 'FJT',
|
||||||
|
altName: 'Fiji Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pacific/Auckland',
|
||||||
|
label: '(GMT+12:00) Auckland, Wellington (NZST)',
|
||||||
|
offset: 12,
|
||||||
|
abbrev: 'NZST',
|
||||||
|
altName: 'New Zealand Standard Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pacific/Tongatapu',
|
||||||
|
label: "(GMT+13:00) Nuku'alofa (TOT)",
|
||||||
|
offset: 13,
|
||||||
|
abbrev: 'TOT',
|
||||||
|
altName: 'Tonga Standard Time',
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,62 @@
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
const FORMAT_GMT = `G[M]TZ`;
|
||||||
|
|
||||||
|
export const getGMTString = timeZone => {
|
||||||
|
if (!timeZone) return moment().format(FORMAT_GMT);
|
||||||
|
return moment()
|
||||||
|
.tz(timeZone)
|
||||||
|
.format(FORMAT_GMT);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDateString = (date, timeZone, dateFormat) => {
|
||||||
|
if (!timeZone) return moment(date).format(dateFormat);
|
||||||
|
return moment(date)
|
||||||
|
.tz(timeZone)
|
||||||
|
.format(dateFormat);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDateStringWithGMT = (date, timeZone, dateFormat) => {
|
||||||
|
if (!timeZone) return moment(date).format(dateFormat);
|
||||||
|
return moment(date)
|
||||||
|
.tz(timeZone)
|
||||||
|
.format(`${dateFormat} (${FORMAT_GMT})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MILLISECONDS_IN_SECOND = 1000;
|
||||||
|
const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * 60;
|
||||||
|
const MILLISECONDS_IN_HOUR = MILLISECONDS_IN_MINUTE * 60;
|
||||||
|
|
||||||
|
export function toMilliseconds(units, value) {
|
||||||
|
switch (units) {
|
||||||
|
case 'h':
|
||||||
|
return Math.round(value * MILLISECONDS_IN_HOUR);
|
||||||
|
case 'm':
|
||||||
|
return Math.round(value * MILLISECONDS_IN_MINUTE);
|
||||||
|
case 's':
|
||||||
|
default:
|
||||||
|
return Math.round(value * MILLISECONDS_IN_SECOND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromMilliseconds(milliseconds) {
|
||||||
|
const round = (value) => parseFloat(value.toFixed(2));
|
||||||
|
if (milliseconds > MILLISECONDS_IN_HOUR) {
|
||||||
|
return {
|
||||||
|
units: "h",
|
||||||
|
value: round(milliseconds / MILLISECONDS_IN_HOUR),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (milliseconds > MILLISECONDS_IN_MINUTE) {
|
||||||
|
return {
|
||||||
|
units: "m",
|
||||||
|
value: round(milliseconds / MILLISECONDS_IN_MINUTE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
units: "s",
|
||||||
|
value: round(milliseconds / MILLISECONDS_IN_SECOND),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { DEFAULT_PAGE_SIZE, ORDER_DESC } from ".";
|
||||||
|
import List from "./List";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
|
const AutoList = (props) => {
|
||||||
|
const { visible, data = [], pagination, searchKey, searchValue, sorter, filters, groups } = props;
|
||||||
|
|
||||||
|
const [currentPagination, setCurrentPagination] = useState();
|
||||||
|
|
||||||
|
const handlePageChange = (newPage) => {
|
||||||
|
const { pages } = currentPagination;
|
||||||
|
if (newPage < 0 || newPage > pages) return;
|
||||||
|
setCurrentPagination({
|
||||||
|
...currentPagination,
|
||||||
|
currentPage: newPage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterData = useMemo(() => {
|
||||||
|
let newData = cloneDeep(data);
|
||||||
|
if (searchKey && searchValue) {
|
||||||
|
newData = newData.filter((item) => `${item[searchKey] || ''}`.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1)
|
||||||
|
}
|
||||||
|
groups.filter((group) => group.value !== undefined && group.value !== '' && !!group.key).forEach((group) => {
|
||||||
|
newData = newData.filter((item) => {
|
||||||
|
if (item.hasOwnProperty(group.key)) {
|
||||||
|
return item[group.key] === group.value
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const keys = Object.keys(filters);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
keys.filter((key) => (filters[key] || []).length !== 0).forEach((key) => {
|
||||||
|
newData = newData.filter((item) => filters[key].indexOf(item[key]) !== -1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (sorter.length >= 2 && sorter[0] && sorter[1]) {
|
||||||
|
const key = sorter[0];
|
||||||
|
const order = sorter[1];
|
||||||
|
newData = newData.sort((a, b) => {
|
||||||
|
if (typeof a[key] ==='string') {
|
||||||
|
return order === ORDER_DESC ? b[key].localeCompare(a[key]) : a[key].localeCompare(b[key])
|
||||||
|
} else if (!isNaN(a[key])) {
|
||||||
|
return order === ORDER_DESC ? b[key] - a[key] : a[key] - b[key]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
}, [JSON.stringify(data), searchKey, searchValue, JSON.stringify(sorter), JSON.stringify(filters), JSON.stringify(groups)])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pagination === false) {
|
||||||
|
setCurrentPagination(false)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const total = filterData.length;
|
||||||
|
const pageSize = pagination?.pageSize || DEFAULT_PAGE_SIZE;
|
||||||
|
setCurrentPagination({
|
||||||
|
currentPage: total ? 1 : 0,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / pageSize)
|
||||||
|
})
|
||||||
|
}, [filterData, pagination])
|
||||||
|
|
||||||
|
const pageData = useMemo(() => {
|
||||||
|
if (!currentPagination) return filterData;
|
||||||
|
const { currentPage, pageSize } = currentPagination;
|
||||||
|
return filterData.filter((item, index) => index >= (currentPage - 1) * pageSize && index < currentPage * pageSize)
|
||||||
|
}, [filterData, currentPagination])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
visible={visible}
|
||||||
|
data={pageData}
|
||||||
|
pagination={currentPagination === false ? false : {
|
||||||
|
...currentPagination,
|
||||||
|
onChange: handlePageChange
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoList
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { DEFAULT_PAGE_SIZE } from ".";
|
||||||
|
import List from "./List";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
const CustomList = (props) => {
|
||||||
|
const { visible, data = [], pagination = {} } = props;
|
||||||
|
|
||||||
|
const total = pagination.total || data.length;
|
||||||
|
const pageSize = pagination.pageSize || DEFAULT_PAGE_SIZE
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
visible={visible}
|
||||||
|
data={data}
|
||||||
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
pageSize,
|
||||||
|
pages: Math.ceil(total / pageSize)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomList
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import DropdownList, { ORDER_DESC } from '.';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
const mock_data = ((size) => {
|
||||||
|
return [...new Array(size).keys()].map((item, index) => {
|
||||||
|
const num = index + 1
|
||||||
|
const type = index % 2 === 0 ? `elasticsearch` : `easysearch`
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
cluster_id: `${num} - ${type}`,
|
||||||
|
cluster_name: `${num} - ${type}`,
|
||||||
|
type,
|
||||||
|
version: `7.0.${index}`,
|
||||||
|
status: 'green'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})(1000)
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
const currentPage = undefined;
|
||||||
|
// const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
// const [total, setTotal] = useState();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [value, setValue] = useState([
|
||||||
|
{
|
||||||
|
cluster_id: '1 - elasticsearch',
|
||||||
|
cluster_name: '1 - elasticsearch',
|
||||||
|
type: 'elasticsearch',
|
||||||
|
version: '7.0.2',
|
||||||
|
status: 'green'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const [searchValue, setSearchValue] = useState()
|
||||||
|
const [sorter, setSorter] = useState(['cluster_name', 'desc'])
|
||||||
|
const [filters, setFilters] = useState({ })
|
||||||
|
const [groups, setGroups] = useState([{ key: 'category', value: 'all' }, { key: 'type', value: 'easysearch'}])
|
||||||
|
|
||||||
|
const fetchData = (currentPage, pageSize, searchValue, sorter, filters, groups) => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!currentPage) {
|
||||||
|
setData(mock_data)
|
||||||
|
setLoading(false)
|
||||||
|
return mock_data
|
||||||
|
};
|
||||||
|
let newData = cloneDeep(mock_data)
|
||||||
|
if (searchValue) {
|
||||||
|
newData = newData.filter((item) => {
|
||||||
|
return item.cluster_name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const keys = Object.keys(filters);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
keys.filter((key) => (filters[key] || []).length !== 0).forEach((key) => {
|
||||||
|
newData = newData.filter((item) => filters[key].indexOf(item[key]) !== -1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.length > 0) {
|
||||||
|
groups.forEach((group) => {
|
||||||
|
newData = newData.filter((item) => {
|
||||||
|
if (item.hasOwnProperty(group.key)) {
|
||||||
|
return item[group.key] === group.value
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sorter.length >= 2 && sorter[0] && sorter[1]) {
|
||||||
|
const key = sorter[0];
|
||||||
|
const order = sorter[1];
|
||||||
|
newData = newData.sort((a, b) => {
|
||||||
|
if (typeof a[key] ==='string') {
|
||||||
|
return order === ORDER_DESC ? b[key].localeCompare(a[key]) : a[key].localeCompare(b[key])
|
||||||
|
} else if (!isNaN(a[key])) {
|
||||||
|
return order === ORDER_DESC ? b[key] - a[key] : a[key] - b[key]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// const total = newData.length
|
||||||
|
setData(newData.filter((item, index) => index >= (currentPage - 1) * pageSize && index < currentPage * pageSize))
|
||||||
|
// setTotal(total)
|
||||||
|
setLoading(false)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
//async
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(currentPage, pageSize, searchValue, sorter, filters, groups)
|
||||||
|
}, [currentPage, pageSize, searchValue, sorter, filters, groups])
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetchData()
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownList
|
||||||
|
width={300}
|
||||||
|
allowClear
|
||||||
|
mode="multiple"
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="cluster_id"
|
||||||
|
data={data}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<>
|
||||||
|
<span style={{ marginRight: 4 }}>
|
||||||
|
<span style={{
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: 'green',
|
||||||
|
display: 'inline-block',
|
||||||
|
}}></span>
|
||||||
|
</span>
|
||||||
|
{item.cluster_name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
renderTag={(item) => <div>{item.type} {item.version}</div>}
|
||||||
|
// pagination={false}
|
||||||
|
// pagination={{
|
||||||
|
// pageSize: 4,
|
||||||
|
// }}
|
||||||
|
// pagination={{
|
||||||
|
// currentPage,
|
||||||
|
// pageSize: pageSize,
|
||||||
|
// total,
|
||||||
|
// onChange: (page) => setCurrentPage(page)
|
||||||
|
// }}
|
||||||
|
// searchKey="cluster_name"
|
||||||
|
onSearchChange={setSearchValue}
|
||||||
|
sorter={sorter}
|
||||||
|
onSorterChange={setSorter}
|
||||||
|
sorterOptions={[
|
||||||
|
{ label: "Cluster Name", key: "cluster_name" },
|
||||||
|
]}
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
filterOptions={[
|
||||||
|
{
|
||||||
|
label: "DISTRIBUTION",
|
||||||
|
key: "type",
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
value: "elasticsearch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "easysearch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
groups={groups}
|
||||||
|
onGroupsChange={setGroups}
|
||||||
|
groupOptions={[
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: 'All',
|
||||||
|
value: 'all',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: 'Easysearch',
|
||||||
|
value: 'easysearch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: 'Elasticsearch',
|
||||||
|
value: 'elasticsearch'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: 'InfiniLabs',
|
||||||
|
value: 'infinilabs',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: 'Easysearch',
|
||||||
|
value: 'easysearch'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Demo;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Empty } from "antd";
|
||||||
|
import styles from "./Empty.less";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import styles from "./Error.less";
|
||||||
|
import { Icon } from "antd";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { currentLocales, failed = true } = props;
|
||||||
|
if (!failed) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<Icon type="close-circle" />
|
||||||
|
<div className={styles.tips}>{currentLocales["dropdownlist.loading.failed"]}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.anticon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ff6c6c;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
color: rgba(191, 191, 191, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,465 @@
|
||||||
|
import { Button, Checkbox, Divider, Icon, Input, Popover, Select } from "antd";
|
||||||
|
import {
|
||||||
|
Fragment,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
|
import Loading from "./Loading";
|
||||||
|
import Error from "./Error";
|
||||||
|
import Empty from "./Empty";
|
||||||
|
|
||||||
|
import styles from "./List.less";
|
||||||
|
|
||||||
|
const List = (props) => {
|
||||||
|
const {
|
||||||
|
visible,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
loading = false,
|
||||||
|
failed = false,
|
||||||
|
rowKey,
|
||||||
|
renderItem,
|
||||||
|
renderTag,
|
||||||
|
renderEmptyList,
|
||||||
|
onSearchChange,
|
||||||
|
searchValue,
|
||||||
|
sorterOptions,
|
||||||
|
pagination,
|
||||||
|
data,
|
||||||
|
sorter,
|
||||||
|
onSorterChange,
|
||||||
|
filters,
|
||||||
|
onFiltersChange,
|
||||||
|
filterOptions,
|
||||||
|
groups,
|
||||||
|
onGroupsChange,
|
||||||
|
groupOptions,
|
||||||
|
currentLocales,
|
||||||
|
showGroup,
|
||||||
|
onShowGroupChange,
|
||||||
|
isMultiple,
|
||||||
|
extraData = [],
|
||||||
|
searchPlaceholder,
|
||||||
|
onRefresh,
|
||||||
|
actions = [],
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [currentSorter, setCurrentSorter] = useState([]);
|
||||||
|
const [sorterVisible, setSorterVisible] = useState(false);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
onShowGroupChange(false);
|
||||||
|
onSearchChange();
|
||||||
|
}, 500);
|
||||||
|
}, [onShowGroupChange, onSearchChange]);
|
||||||
|
|
||||||
|
const handleChange = (item) => {
|
||||||
|
if (item.disabled) return;
|
||||||
|
if (isMultiple) {
|
||||||
|
const newValue = cloneDeep(value) || [];
|
||||||
|
const index = newValue.findIndex((v) => v[rowKey] === item[rowKey]);
|
||||||
|
if (index === -1) {
|
||||||
|
newValue.push(item);
|
||||||
|
} else {
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
}
|
||||||
|
onChange(newValue);
|
||||||
|
} else {
|
||||||
|
onChange(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModeChange = () => {
|
||||||
|
onShowGroupChange(!showGroup);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupChange = (level, group) => {
|
||||||
|
const newGroups = [...groups];
|
||||||
|
if (newGroups[level]?.value === group.value) {
|
||||||
|
newGroups.splice(level, 1);
|
||||||
|
} else {
|
||||||
|
newGroups[level] = {
|
||||||
|
key: group.key,
|
||||||
|
value: group.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
newGroups.splice(level + 1, newGroups.length);
|
||||||
|
onGroupsChange(newGroups);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value) => {
|
||||||
|
onSearchChange(value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (value, index) => {
|
||||||
|
const newSorter = [...currentSorter];
|
||||||
|
newSorter[index] = value;
|
||||||
|
setCurrentSorter(newSorter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFiltersChange = (field, value, checked) => {
|
||||||
|
const newFilters = cloneDeep(filters);
|
||||||
|
const item = newFilters[field] || [];
|
||||||
|
const index = item.indexOf(value);
|
||||||
|
if (index === -1) {
|
||||||
|
if (checked) item.push(value);
|
||||||
|
} else {
|
||||||
|
if (!checked) item.splice(index, 1);
|
||||||
|
}
|
||||||
|
newFilters[field] = item;
|
||||||
|
onFiltersChange(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckAllChange = (checked) => {
|
||||||
|
const newValue = cloneDeep(value || []);
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.disabled) return;
|
||||||
|
const index = newValue.findIndex((v) => v[rowKey] === item[rowKey]);
|
||||||
|
if (index === -1 && checked) {
|
||||||
|
newValue.push(item);
|
||||||
|
} else if (index !== -1 && !checked) {
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && visible) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [visible, inputRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}, [visible, handleClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSorter(sorter);
|
||||||
|
}, [sorter]);
|
||||||
|
|
||||||
|
const { currentPage, pageSize, total, pages } = pagination;
|
||||||
|
|
||||||
|
const groupChildOptions = useMemo(() => {
|
||||||
|
if (groups[0] === undefined) return [];
|
||||||
|
const child = groupOptions.find((item) => item.value === groups[0].value);
|
||||||
|
return child?.list || [];
|
||||||
|
}, [groupOptions, groups]);
|
||||||
|
|
||||||
|
const isCheckAll = useMemo(() => {
|
||||||
|
if (!isMultiple) return false;
|
||||||
|
if (isMultiple) {
|
||||||
|
if (value?.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.every(
|
||||||
|
(item) => value.findIndex((v) => v[rowKey] === item[rowKey]) !== -1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.some(
|
||||||
|
(item) => value.findIndex((v) => v[rowKey] === item[rowKey]) !== -1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "indeterminate";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [isMultiple, JSON.stringify(data), JSON.stringify(value)]);
|
||||||
|
|
||||||
|
const renderGroupOptions = (options, level) => {
|
||||||
|
if (!showGroup || options.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.group}>
|
||||||
|
{options.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={`${styles.item} ${
|
||||||
|
groups[level]?.value === item.value ? styles.selected : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleGroupChange(level, item)}
|
||||||
|
title={item.label}
|
||||||
|
>
|
||||||
|
<span className={styles.label}>{item.label}</span>
|
||||||
|
<Icon type="right" className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const newActions = actions.concat(
|
||||||
|
onRefresh
|
||||||
|
? [
|
||||||
|
<a
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
if (!loading) {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLocales["dropdownlist.refresh"]}
|
||||||
|
</a>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{renderGroupOptions(groupOptions, 0)}
|
||||||
|
{renderGroupOptions(groupChildOptions, 1)}
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!loading) {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
handleSearch(newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
className={styles.search}
|
||||||
|
placeholder={
|
||||||
|
searchPlaceholder ||
|
||||||
|
currentLocales["dropdownlist.search.placeholder"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={styles.tools}>
|
||||||
|
<div className={styles.result}>
|
||||||
|
{currentLocales["dropdownlist.result.found"]}{" "}
|
||||||
|
{pagination === false ? data.length : total}{" "}
|
||||||
|
{currentLocales["dropdownlist.result.records"]}
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{sorterOptions.length > 0 && (
|
||||||
|
<Popover
|
||||||
|
visible={sorterVisible}
|
||||||
|
onVisibleChange={setSorterVisible}
|
||||||
|
overlayClassName={styles.filterPopover}
|
||||||
|
placement="bottom"
|
||||||
|
trigger={"click"}
|
||||||
|
content={
|
||||||
|
<div className={styles.sorter}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{currentLocales["dropdownlist.sort.by"]}
|
||||||
|
</div>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<Select
|
||||||
|
value={currentSorter[0]}
|
||||||
|
style={{ width: "65%" }}
|
||||||
|
onChange={(value) => handleSortChange(value, 0)}
|
||||||
|
>
|
||||||
|
{sorterOptions.map((item) => (
|
||||||
|
<Select.Option key={item.key} value={item.key}>
|
||||||
|
{item.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={currentSorter[1]}
|
||||||
|
style={{ width: "35%" }}
|
||||||
|
onChange={(value) => handleSortChange(value, 1)}
|
||||||
|
>
|
||||||
|
<Select.Option value="desc">
|
||||||
|
{currentLocales["dropdownlist.sort.by.desc"]}
|
||||||
|
</Select.Option>
|
||||||
|
<Select.Option value="asc">
|
||||||
|
{currentLocales["dropdownlist.sort.by.asc"]}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
onSorterChange([]);
|
||||||
|
setSorterVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLocales["dropdownlist.sort.by.clear"]}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{ width: 80 }}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onSorterChange(currentSorter);
|
||||||
|
setSorterVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLocales["dropdownlist.apply"]}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon style={{ cursor: "pointer" }} type="sort-ascending" />
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
{filterOptions.length > 0 && (
|
||||||
|
<Popover
|
||||||
|
overlayClassName={styles.filterPopover}
|
||||||
|
placement="bottom"
|
||||||
|
trigger={"click"}
|
||||||
|
content={
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{currentLocales["dropdownlist.filters"]}
|
||||||
|
<Icon
|
||||||
|
className={styles.clear}
|
||||||
|
type="reload"
|
||||||
|
onClick={() => {
|
||||||
|
onFiltersChange({});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filterOptions.map((item) => (
|
||||||
|
<div key={item.key} className={styles.content}>
|
||||||
|
<div className={styles.label}>{item.label}</div>
|
||||||
|
<div className={styles.options}>
|
||||||
|
{(item.list || []).map((c) => (
|
||||||
|
<div key={c.value} className={styles.option}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFiltersChange(
|
||||||
|
item.key,
|
||||||
|
c.value,
|
||||||
|
e.target.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
checked={
|
||||||
|
(filters[item.key] || []).indexOf(c.value) !==
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{c.label || c.value}
|
||||||
|
</Checkbox>
|
||||||
|
{c.count !== undefined && <div>{c.count}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon style={{ cursor: "pointer" }} type="filter" />
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
{groupOptions.length > 0 && (
|
||||||
|
<Icon
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
type="layout"
|
||||||
|
onClick={handleModeChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isMultiple && (
|
||||||
|
<Checkbox
|
||||||
|
indeterminate={isCheckAll === "indeterminate"}
|
||||||
|
onChange={(e) => handleCheckAllChange(e.target.checked)}
|
||||||
|
checked={isCheckAll === true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.listWrapper}>
|
||||||
|
<div
|
||||||
|
className={styles.list}
|
||||||
|
style={{ maxHeight: `${32 * (pageSize + extraData.length)}px` }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Loading currentLocales={currentLocales} />
|
||||||
|
) : failed ? (
|
||||||
|
<Error currentLocales={currentLocales} />
|
||||||
|
) : data.length === 0 && extraData.length === 0 ? (
|
||||||
|
renderEmptyList ? (
|
||||||
|
renderEmptyList()
|
||||||
|
) : (
|
||||||
|
<Empty />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
extraData.concat(data).map((item) => {
|
||||||
|
const isSelected = isMultiple
|
||||||
|
? (value || []).findIndex(
|
||||||
|
(v) => v[rowKey] === item[rowKey]
|
||||||
|
) !== -1
|
||||||
|
: value?.[rowKey] === item[rowKey];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item[rowKey]}
|
||||||
|
className={`${styles.item} ${
|
||||||
|
isSelected ? styles.selected : ""
|
||||||
|
} ${
|
||||||
|
item.disabled ? styles.disabled : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleChange(item)}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>
|
||||||
|
{renderItem ? renderItem(item) : item[rowKey]}
|
||||||
|
</div>
|
||||||
|
{renderTag && !showGroup ? (
|
||||||
|
<div className={styles.tag}>
|
||||||
|
{renderItem ? renderTag(item) : item[rowKey]}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{isSelected && <Icon type="check" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{pagination === false ? null : (
|
||||||
|
<div className={styles.footerWrapper}>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{newActions.map((item, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{item}
|
||||||
|
{index !== newActions.length - 1 && (
|
||||||
|
<Divider type="vertical" />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.pager}>
|
||||||
|
<Icon
|
||||||
|
onClick={() => pagination.onChange(currentPage - 1)}
|
||||||
|
type="left"
|
||||||
|
className={`${styles.icon} ${
|
||||||
|
currentPage <= 1 ? styles.disabled : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className={styles.pageNum}>
|
||||||
|
{currentPage}/{pages}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
onClick={() => pagination.onChange(currentPage + 1)}
|
||||||
|
type="right"
|
||||||
|
className={`${styles.icon} ${
|
||||||
|
currentPage >= pages ? styles.disabled : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
|
@ -0,0 +1,233 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.group {
|
||||||
|
border-right: 1px solid #ebebeb;
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 0 8px 0 16px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0px 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.result {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
color: #101010;
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-checkbox-inner{
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-color: rgba(0,0,0,0.65);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-checked .ant-checkbox-inner:after {
|
||||||
|
top: 5px;
|
||||||
|
left: 2px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-indeterminate .ant-checkbox-inner::after {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listWrapper {
|
||||||
|
margin: 12px -6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
.item {
|
||||||
|
height: 32px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 6px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
color: #999
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerWrapper {
|
||||||
|
margin: 0px -16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid #ebebeb;
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
|
||||||
|
.pageNum {
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #007fff;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: #c1c1c1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterPopover {
|
||||||
|
:global {
|
||||||
|
.ant-popover-title {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorter {
|
||||||
|
width: 250px;
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
width: 250px;
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
color: #007fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Loading from "./loading.svg";
|
||||||
|
import styles from "./Loading.less";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { loading = true, currentLocales } = props;
|
||||||
|
if (!loading) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<img src={Loading}/>
|
||||||
|
<div className={styles.tips}>{currentLocales["dropdownlist.loading"]}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
color: rgba(191, 191, 191, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
import { Button, Icon, Popover } from "antd";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
|
import AutoList from "./AutoList";
|
||||||
|
import CustomList from "./CustomList";
|
||||||
|
import locales from "./locales";
|
||||||
|
|
||||||
|
import styles from "./index.less";
|
||||||
|
|
||||||
|
export const DEFAULT_PAGE_SIZE = 10;
|
||||||
|
export const ORDER_DESC = "desc";
|
||||||
|
export const ORDER_ASC = "asc";
|
||||||
|
|
||||||
|
const DropdownList = (props) => {
|
||||||
|
const {
|
||||||
|
className = "",
|
||||||
|
popoverClassName = "",
|
||||||
|
popoverPlacement = "bottomLeft",
|
||||||
|
children,
|
||||||
|
width = 300,
|
||||||
|
dropdownWidth,
|
||||||
|
locale = "en-US",
|
||||||
|
allowClear = false,
|
||||||
|
mode = "",
|
||||||
|
value,
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
placeholder = "",
|
||||||
|
loading = false,
|
||||||
|
data = [],
|
||||||
|
rowKey,
|
||||||
|
renderItem,
|
||||||
|
renderLabel,
|
||||||
|
pagination,
|
||||||
|
onSearchChange = () => {},
|
||||||
|
sorter = [],
|
||||||
|
onSorterChange = () => {},
|
||||||
|
sorterOptions = [],
|
||||||
|
filters = {},
|
||||||
|
onFiltersChange = () => {},
|
||||||
|
filterOptions = [],
|
||||||
|
groups = [],
|
||||||
|
onGroupsChange = () => {},
|
||||||
|
groupOptions = [],
|
||||||
|
onGroupVisibleChange = () => {},
|
||||||
|
autoAdjustOverflow = {
|
||||||
|
adjustX: 1,
|
||||||
|
},
|
||||||
|
getPopupContainer = (triggerNode) => triggerNode.parentNode,
|
||||||
|
extraData = [],
|
||||||
|
showListIcon = true,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [showGroup, setShowGroup] = useState(false);
|
||||||
|
const [searchValue, setSearchValue] = useState();
|
||||||
|
const [fixedDropdownWidth, setFixedDropdownWidth] = useState();
|
||||||
|
const domRef = useRef(null);
|
||||||
|
|
||||||
|
const isMultiple = useMemo(() => {
|
||||||
|
return mode === "multiple" && (value === undefined || Array.isArray(value));
|
||||||
|
}, [mode, value]);
|
||||||
|
|
||||||
|
const handleVisible = (visible) => {
|
||||||
|
setVisible(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (item) => {
|
||||||
|
onChange(item);
|
||||||
|
if (!isMultiple) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value) => {
|
||||||
|
setSearchValue(value);
|
||||||
|
onSearchChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultipleRemove = (index) => {
|
||||||
|
const newValue = cloneDeep(value) || [];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onChange(isMultiple ? [] : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
const newProps = {
|
||||||
|
...props,
|
||||||
|
value,
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
onSearchChange: handleSearch,
|
||||||
|
searchValue,
|
||||||
|
sorter,
|
||||||
|
onSorterChange,
|
||||||
|
sorterOptions,
|
||||||
|
filters,
|
||||||
|
onFiltersChange,
|
||||||
|
filterOptions,
|
||||||
|
groups,
|
||||||
|
onGroupsChange,
|
||||||
|
groupOptions,
|
||||||
|
visible,
|
||||||
|
onChange: handleChange,
|
||||||
|
currentLocales: locales[locale] || {},
|
||||||
|
showGroup,
|
||||||
|
onShowGroupChange: (visible) => {
|
||||||
|
setShowGroup(visible);
|
||||||
|
onGroupVisibleChange(visible);
|
||||||
|
},
|
||||||
|
isMultiple,
|
||||||
|
};
|
||||||
|
if (!pagination || !pagination.currentPage || !pagination.total) {
|
||||||
|
return <AutoList {...newProps} />;
|
||||||
|
}
|
||||||
|
return <CustomList {...newProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSingleValue = (value) => {
|
||||||
|
return renderLabel
|
||||||
|
? renderLabel(value)
|
||||||
|
: renderItem
|
||||||
|
? renderItem(value)
|
||||||
|
: value[rowKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderValue = () => {
|
||||||
|
const defaultLabel = <span style={{ color: "#999" }}>{placeholder}</span>;
|
||||||
|
if (!isMultiple) {
|
||||||
|
return value && value[rowKey] ? renderSingleValue(value) : defaultLabel;
|
||||||
|
}
|
||||||
|
return value?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{value
|
||||||
|
.filter((item) => !!(item && item[rowKey]))
|
||||||
|
.map((item, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={`${styles.multipleItem} common-ui-dropdownlist-multiple-item`}
|
||||||
|
style={{ backgroundColor: disabled ? "#c4c4c4" : "#fff" }}
|
||||||
|
>
|
||||||
|
{renderSingleValue(item)}
|
||||||
|
{
|
||||||
|
!item.disabled ? (
|
||||||
|
<Icon
|
||||||
|
type="close"
|
||||||
|
style={{ marginLeft: 4, fontSize: 10 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleMultipleRemove(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
defaultLabel
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dropdownWidth && isNaN(Number(width)) && domRef.current) {
|
||||||
|
setFixedDropdownWidth(domRef.current.offsetWidth);
|
||||||
|
}
|
||||||
|
}, [dropdownWidth, width, domRef]);
|
||||||
|
|
||||||
|
let overlayWidth = dropdownWidth || width;
|
||||||
|
|
||||||
|
if (isNaN(Number(overlayWidth))) {
|
||||||
|
if (fixedDropdownWidth) {
|
||||||
|
overlayWidth = `${fixedDropdownWidth}px`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
overlayWidth = `${overlayWidth}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showGroup) {
|
||||||
|
overlayWidth = `calc(${overlayWidth} + ${120 * groups.length || 120}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatValue = renderValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.dropdownList} ${className}`}
|
||||||
|
style={{ width }}
|
||||||
|
ref={domRef}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
visible={disabled ? false : visible}
|
||||||
|
onVisibleChange={handleVisible}
|
||||||
|
placement={popoverPlacement}
|
||||||
|
content={renderContent()}
|
||||||
|
trigger={"click"}
|
||||||
|
overlayClassName={`${styles.popover} ${popoverClassName}`}
|
||||||
|
overlayStyle={{
|
||||||
|
width: overlayWidth,
|
||||||
|
maxHeight:
|
||||||
|
16 + 32 + 8 + 18 + 12 + 40 + (pagination?.pageSize || 10) * 32,
|
||||||
|
}}
|
||||||
|
autoAdjustOverflow={autoAdjustOverflow}
|
||||||
|
getPopupContainer={getPopupContainer}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`${styles.button} common-ui-dropdownlist-select ${
|
||||||
|
allowClear ? styles.allowClear : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
{showListIcon && <Icon type="bars" className={styles.icon} />}
|
||||||
|
<span>{formatValue}</span>
|
||||||
|
</div>
|
||||||
|
<Icon type={visible ? "up" : "down"} className={styles.down} />
|
||||||
|
{allowClear && (
|
||||||
|
<Icon
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClear();
|
||||||
|
}}
|
||||||
|
theme="filled"
|
||||||
|
type="close-circle"
|
||||||
|
className={styles.close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownList;
|
|
@ -0,0 +1,86 @@
|
||||||
|
.dropdownList {
|
||||||
|
padding: auto;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-btn {
|
||||||
|
padding: 0 8px;
|
||||||
|
border-color: #ebebeb;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.ant-btn, .ant-btn.ant-popover-open, .ant-btn:hover {
|
||||||
|
background-color: #e8f6fe;
|
||||||
|
}
|
||||||
|
.ant-btn[disabled],
|
||||||
|
.ant-btn[disabled]:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #bbb;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #101010;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.down {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: rgba(0,0,0,.25);
|
||||||
|
font-size: 12px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multipleItem {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 0 4px;
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.allowClear {
|
||||||
|
&:hover {
|
||||||
|
.down {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
padding-top: 8px;
|
||||||
|
:global {
|
||||||
|
.ant-popover-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### DropdownList
|
||||||
|
|
||||||
|
| Property | Description | Type | Default | Version |
|
||||||
|
| -------- | ----------- | ---- | ------- | ------- |
|
||||||
|
| className | 组件根元素的类名称 | string | - | 1.0.0 |
|
||||||
|
| popoverClassName | 弹窗元素的类名称 | string | - | 1.0.0 |
|
||||||
|
| popoverPlacement | 弹窗位置,可选 top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom | string | 'bottomLeft' | 1.0.0 |
|
||||||
|
| width | 选择框宽度 | number | 300 | 1.0.0 |
|
||||||
|
| dropdownWidth | 下拉框宽度,默认与选择框宽度一样 | number | 300 | 1.0.0 |
|
||||||
|
| locale | 语言 | string | 'en-US' | 1.0.0 |
|
||||||
|
| allowClear | 可以点击清除图标删除内容 | boolean | false | 1.0.0 |
|
||||||
|
| mode | 设置模式为多选 | 'multiple' | - | 1.0.0 |
|
||||||
|
| value | 已选项 | object | - | 1.0.0 |
|
||||||
|
| onChange | 选中变更的回调 | (value: object ) => void | - | 1.0.0 |
|
||||||
|
| disabled | 是否禁用 | boolean | false | 1.0.0 |
|
||||||
|
| placeholder | 选择框默认文字 | string | - | 1.0.0 |
|
||||||
|
| loading | 是否加载中 | boolean | false | 1.0.0 |
|
||||||
|
| failed | 是否加载失败 | boolean | false | 1.0.0 |
|
||||||
|
| data | 列表数据 | [] | [] | 1.0.0 |
|
||||||
|
| rowKey | 列表行key | string | - | 1.0.0 |
|
||||||
|
| renderItem | 列表行自定义渲染 | (item: object) => ReactNode | - | 1.0.0 |
|
||||||
|
| renderTag | 列表行自定义标签 | (item: object) => ReactNode | - | 1.0.0 |
|
||||||
|
| renderLabel | 选择框文本自定义渲染 | (item: object) => ReactNode | - | 1.0.0 |
|
||||||
|
| renderEmptyList | 空列表自定义渲染 | () => ReactNode | - | 1.0.0 |
|
||||||
|
| pagination | 分页器,设为 false 时不展示和进行分页 | { currentPage: number, pageSize: number, total: number, onChange: (page: number) => void } | { currentPage: 1, pageSize: 10 } | 1.0.0 |
|
||||||
|
| searchKey | 搜索字段,自动分页起效 | string | - | 1.0.0 |
|
||||||
|
| onSearchChange | 搜索变更的回调 | (value: string) => void | - | 1.0.0 |
|
||||||
|
| sorter | 排序,第一个元素为排序字段,第二个元素为排序方式(desc/asc) | [] | [] | 1.0.0 |
|
||||||
|
| onSorterChange | 排序变更的回调 | (sorter: []) => void | - | 1.0.0 |
|
||||||
|
| sorterOptions | 排序选项 | [{ label: string, key: string }] | [] | 1.0.0 |
|
||||||
|
| filters | 字段过滤 | object | {} | 1.0.0 |
|
||||||
|
| onFiltersChange | 字段过滤变更的回调 | (filters: object) => void | - | 1.0.0 |
|
||||||
|
| filterOptions | 字段过滤选项 | [] | [] | 1.0.0 |
|
||||||
|
| groups | 分组过滤 | [] | [] | 1.0.0 |
|
||||||
|
| onGroupsChange | 分组过滤变更的回调 | (groups: []) => void | - | 1.0.0 |
|
||||||
|
| groupOptions | 分组过滤选项 | [] | [] | 1.0.0 |
|
||||||
|
| onGroupVisibleChange | 分组显隐变更的回调 | (visible: boolean) => void | - | 1.0.0 |
|
||||||
|
| autoAdjustOverflow | 浮窗被遮挡时自动调整位置 | boolean \| { adjustX?: 0 \| 1, adjustY?: 0 \| 1 } | { adjustX: 1 } | 1.0.0 |
|
||||||
|
| getPopupContainer | 浮层渲染父节点 | (triggerNode) => element | () => document.body | 1.0.0 |
|
||||||
|
| extraData | 额外项数据,置顶显示 | object[] | [] | 1.0.0 |
|
||||||
|
| searchPlaceholder | 下拉列表搜索框默认文字 | string | - | 1.0.0 |
|
||||||
|
| showListIcon | 是否显示左侧图标 | boolean | true | 1.0.0 |
|
||||||
|
| onRefresh | 列表数据刷新的回调 | () => void | - | 1.0.0 |
|
||||||
|
| actions | 下拉列表底部左侧操作 | ReactNode[] | [] | 1.0.0 |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<svg id="编组" width="201" height="169" viewBox="0 0 201 169" fill="none" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; max-height: 100%;">
|
||||||
|
<path d="M151.456 73.9941H50.4559C25.3269 73.9941 4.9559 94.3652 4.9559 119.494C4.9559 144.623 25.3269 164.994 50.4559 164.994H151.456C176.585 164.994 196.956 144.623 196.956 119.494C196.956 94.3652 176.585 73.9941 151.456 73.9941Z" stroke="#DADADA" stroke-width="8" stroke-dasharray="0,0,0,487.9254150390625">
|
||||||
|
<animate attributeType="XML" attributeName="stroke-dasharray" repeatCount="indefinite" dur="2.5s" values="0,0,0,487.9254150390625; 0,243.96270751953125,243.96270751953125,0; 487.9254150390625,0,0,0; 487.9254150390625,0,0,0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
<animate attributeType="XML" attributeName="opacity" repeatCount="indefinite" dur="2.5s" values="1;1;1;0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
</path>
|
||||||
|
<path d="M188.956 145.994L39.6152 74.269" stroke="#DADADA" stroke-width="8" stroke-dasharray="0,0,0,165.67181396484375">
|
||||||
|
<animate attributeType="XML" attributeName="stroke-dasharray" repeatCount="indefinite" dur="2.5s" values="0,0,0,165.67181396484375; 0,82.83590698242188,82.83590698242188,0; 165.67181396484375,0,0,0; 165.67181396484375,0,0,0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
<animate attributeType="XML" attributeName="opacity" repeatCount="indefinite" dur="2.5s" values="1;1;1;0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
</path>
|
||||||
|
<path d="M40.1212 75.4677C81.667 52.5069 104.876 39.8644 109.747 37.5402C117.054 34.0538 150.997 26.0252 169.353 56.0201C175.128 65.4571 177.702 74.4539 177.074 83.0105" stroke="#DADADA" stroke-width="8" stroke-dasharray="0,0,0,175.83224487304688">
|
||||||
|
<animate attributeType="XML" attributeName="stroke-dasharray" repeatCount="indefinite" dur="2.5s" values="0,0,0,175.83224487304688; 0,87.91612243652344,87.91612243652344,0; 175.83224487304688,0,0,0; 175.83224487304688,0,0,0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
<animate attributeType="XML" attributeName="opacity" repeatCount="indefinite" dur="2.5s" values="1;1;1;0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
</path>
|
||||||
|
<path d="M32.2912 77.5453C48.1729 52.9995 57.2963 38.1487 59.5319 35.0136C64.2324 28.422 90.0277 4.9438 120.624 22.2788C125.755 25.1859 130.629 30.4688 134.047 34.0392" stroke="#DADADA" stroke-width="8" stroke-dasharray="0,0,0,136.42384338378906">
|
||||||
|
<animate attributeType="XML" attributeName="stroke-dasharray" repeatCount="indefinite" dur="2.5s" values="0,0,0,136.42384338378906; 0,68.21192169189453,68.21192169189453,0; 136.42384338378906,0,0,0; 136.42384338378906,0,0,0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
<animate attributeType="XML" attributeName="opacity" repeatCount="indefinite" dur="2.5s" values="1;1;1;0" keyTimes="0; 0.5; 0.8; 1" fill="freeze"></animate>
|
||||||
|
</path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,16 @@
|
||||||
|
const en_us = {
|
||||||
|
"dropdownlist.search.placeholder": "Search...",
|
||||||
|
"dropdownlist.result.found": "Found",
|
||||||
|
"dropdownlist.result.records": "records",
|
||||||
|
"dropdownlist.sort.by": "Sort By",
|
||||||
|
"dropdownlist.sort.by.desc": "Desc",
|
||||||
|
"dropdownlist.sort.by.asc": "Asc",
|
||||||
|
"dropdownlist.sort.by.clear": "Clear sorting",
|
||||||
|
"dropdownlist.filters": "Filters",
|
||||||
|
"dropdownlist.apply": "Apply",
|
||||||
|
"dropdownlist.loading": "Loading...",
|
||||||
|
"dropdownlist.loading.failed": "Load failed",
|
||||||
|
"dropdownlist.refresh": "Refresh",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default en_us
|
|
@ -0,0 +1,9 @@
|
||||||
|
import en_us from './en-US';
|
||||||
|
import zh_cn from './zh-CN';
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
'en-US': en_us,
|
||||||
|
'zh-CN': zh_cn
|
||||||
|
}
|
||||||
|
|
||||||
|
export default locales
|
|
@ -0,0 +1,16 @@
|
||||||
|
const zh_cn = {
|
||||||
|
"dropdownlist.search.placeholder": "搜索",
|
||||||
|
"dropdownlist.result.found": "找到",
|
||||||
|
"dropdownlist.result.records": "结果",
|
||||||
|
"dropdownlist.sort.by": "排序",
|
||||||
|
"dropdownlist.sort.by.desc": "降序",
|
||||||
|
"dropdownlist.sort.by.asc": "升序",
|
||||||
|
"dropdownlist.sort.by.clear": "清除排序",
|
||||||
|
"dropdownlist.filters": "过滤",
|
||||||
|
"dropdownlist.apply": "应用",
|
||||||
|
"dropdownlist.loading": "加载中...",
|
||||||
|
"dropdownlist.loading.failed": "加载失败",
|
||||||
|
"dropdownlist.refresh": "刷新",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default zh_cn
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const Example = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.example}>
|
||||||
|
<Typography>
|
||||||
|
<Title>Introduction</Title>
|
||||||
|
<Paragraph>This is an example!</Paragraph>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Example;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.example {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Component Name
|
||||||
|
|
||||||
|
| Property | Description | Type | Default | Version |
|
||||||
|
| -------- | ----------- | ---- | ------- | ------- |
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
const Stop = () => {
|
||||||
|
return (
|
||||||
|
<svg fill="currentColor" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6680" width="1em" height="1em"><path d="M185.1750275 180.79799469l661.17412406 0 0 661.1395875-661.17412406 0 0-661.1395875Z" p-id="6681"></path></svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Stop;
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'antd/dist/antd.min.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import Example from './Example';
|
||||||
|
|
||||||
|
ReactDOM.render(<Example />, document.getElementById('root'));
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Example from './Example';
|
||||||
|
|
||||||
|
export { Example };
|
Loading…
Reference in New Issue