chore: add web/src/common

This commit is contained in:
luohoufu 2024-12-02 14:30:54 +08:00
parent cc72e736f7
commit bb13650913
No known key found for this signature in database
GPG Key ID: 9D8E0A78772AB5A0
85 changed files with 6423 additions and 0 deletions

23
web/src/common/.gitignore vendored Normal file
View File

@ -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*

15
web/src/common/README.md Normal file
View File

@ -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.

View File

@ -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 were 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;

View File

@ -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;

View File

@ -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,
});

View File

@ -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';
},
};

View File

@ -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};`;
},
};

View File

@ -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();

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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');
};

View File

@ -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 wont 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));
},
};
};

13
web/src/common/dist/asset-manifest.json vendored Normal file
View File

@ -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"
]
}

3
web/src/common/dist/common.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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

1
web/src/common/dist/common.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
web/src/common/dist/index.html vendored Normal file
View File

@ -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

156
web/src/common/package.json Normal file
View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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);
});

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,5 @@
.apply {
.applyBtn {
width: 80px;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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 |

View File

@ -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",
};

View File

@ -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
}

View File

@ -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": "应用",
};

View File

@ -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);
}
};
}

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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();

View File

@ -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}`;
};

View File

@ -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',
};

View File

@ -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',
},
];

View File

@ -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),
};
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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>
)
}

View File

@ -0,0 +1,8 @@
.empty {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
flex-direction: column;
}

View File

@ -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>
)
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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>
)
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,3 @@
.example {
width: 100%;
}

View File

@ -0,0 +1,7 @@
## API
### Component Name
| Property | Description | Type | Default | Version |
| -------- | ----------- | ---- | ------- | ------- |

View File

@ -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;

View File

@ -0,0 +1,5 @@
@import 'antd/dist/antd.min.css';
body {
margin: 0;
}

View File

@ -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'));

View File

@ -0,0 +1,3 @@
import Example from './Example';
export { Example };