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