mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[TableListView] Improve help text of creator and view count (#202488)
## Summary This PR brings back version mentions in help text for non-serverless that we removed in https://github.com/elastic/kibana/pull/193024. In that PR we decided that it is not worth adding complexity for checking `isServerless` deep inside table list view components, but I want to bring version mentions back now because I believe that it can be very confusing without the version mentions for existing deployments Two recent features: 1. created_by; 2. view counts are only working since 8.14 and 8.16 respectively, so for older kibana with old dashboards it might be confusing that the data for new features is missing after the upgrade. In help text we can at least mention that the reason that data is missing is because we only gather the data starting from a specific version. ### Serverless (version mentions are missing as before)   ### Statefull (version are shown again, just like before https://github.com/elastic/kibana/pull/193024)    # Release Notes Improve help text of creator and view count features on dashboard listing page
This commit is contained in:
parent
efe06a3357
commit
ea1c846e54
16 changed files with 160 additions and 48 deletions
|
@ -22,12 +22,15 @@ import {
|
|||
import { getUserDisplayName } from '@kbn/user-profile-components';
|
||||
|
||||
import { Item } from '../types';
|
||||
import { useServices } from '../services';
|
||||
|
||||
export interface ActivityViewProps {
|
||||
item: Pick<Partial<Item>, 'createdBy' | 'createdAt' | 'updatedBy' | 'updatedAt' | 'managed'>;
|
||||
entityNamePlural?: string;
|
||||
}
|
||||
|
||||
export const ActivityView = ({ item }: ActivityViewProps) => {
|
||||
export const ActivityView = ({ item, entityNamePlural }: ActivityViewProps) => {
|
||||
const isKibanaVersioningEnabled = useServices()?.isKibanaVersioningEnabled ?? false;
|
||||
const showLastUpdated = Boolean(item.updatedAt && item.updatedAt !== item.createdAt);
|
||||
|
||||
const UnknownUserLabel = (
|
||||
|
@ -62,7 +65,10 @@ export const ActivityView = ({ item }: ActivityViewProps) => {
|
|||
) : (
|
||||
<>
|
||||
{UnknownUserLabel}
|
||||
<NoCreatorTip />
|
||||
<NoCreatorTip
|
||||
includeVersionTip={isKibanaVersioningEnabled}
|
||||
entityNamePlural={entityNamePlural}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -85,7 +91,10 @@ export const ActivityView = ({ item }: ActivityViewProps) => {
|
|||
) : (
|
||||
<>
|
||||
{UnknownUserLabel}
|
||||
<NoUpdaterTip />
|
||||
<NoUpdaterTip
|
||||
includeVersionTip={isKibanaVersioningEnabled}
|
||||
entityNamePlural={entityNamePlural}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -73,24 +73,39 @@ export const ViewsStats = ({ item }: { item: Item }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const NoViewsTip = () => (
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate('contentManagement.contentEditor.viewsStats.noViewsTipAriaLabel', {
|
||||
defaultMessage: 'Additional information',
|
||||
})}
|
||||
position="top"
|
||||
color="inherit"
|
||||
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
|
||||
css={{ textWrap: 'balance' }}
|
||||
type="questionInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.viewsStats.noViewsTip"
|
||||
defaultMessage="Views are counted every time someone opens a dashboard"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
const NoViewsTip = () => {
|
||||
const isKibanaVersioningEnabled = useServices()?.isKibanaVersioningEnabled ?? false;
|
||||
return (
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate('contentManagement.contentEditor.viewsStats.noViewsTipAriaLabel', {
|
||||
defaultMessage: 'Additional information',
|
||||
})}
|
||||
position="top"
|
||||
color="inherit"
|
||||
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
|
||||
css={{ textWrap: 'balance' }}
|
||||
type="questionInCircle"
|
||||
content={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.viewsStats.noViewsTip"
|
||||
defaultMessage="Views are counted every time someone opens a dashboard"
|
||||
/>
|
||||
{isKibanaVersioningEnabled && (
|
||||
<>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.viewsStats.noViewsVersionTip"
|
||||
defaultMessage="(after version {version})"
|
||||
values={{ version: '8.16' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function getTotalDays(stats: ContentInsightsStats) {
|
||||
return moment.utc().diff(moment.utc(stats.from), 'days');
|
||||
|
|
|
@ -17,6 +17,11 @@ import { ContentInsightsClientPublic } from './client';
|
|||
*/
|
||||
export interface ContentInsightsServices {
|
||||
contentInsightsClient: ContentInsightsClientPublic;
|
||||
/**
|
||||
* Whether versioning is enabled for the current kibana instance. (aka is Serverless)
|
||||
* This is used to determine if we should show the version mentions in the help text.
|
||||
*/
|
||||
isKibanaVersioningEnabled: boolean;
|
||||
}
|
||||
|
||||
const ContentInsightsContext = React.createContext<ContentInsightsServices | null>(null);
|
||||
|
@ -34,7 +39,10 @@ export const ContentInsightsProvider: FC<PropsWithChildren<Partial<ContentInsigh
|
|||
|
||||
return (
|
||||
<ContentInsightsContext.Provider
|
||||
value={{ contentInsightsClient: services.contentInsightsClient }}
|
||||
value={{
|
||||
contentInsightsClient: services.contentInsightsClient,
|
||||
isKibanaVersioningEnabled: services.isKibanaVersioningEnabled ?? false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ContentInsightsContext.Provider>
|
||||
|
|
|
@ -32,6 +32,7 @@ export const getMockServices = (overrides?: Partial<Services & UserProfilesServi
|
|||
isFavoritesEnabled: () => false,
|
||||
bulkGetUserProfiles: async () => [],
|
||||
getUserProfile: async () => ({ uid: '', enabled: true, data: {}, user: { username: '' } }),
|
||||
isKibanaVersioningEnabled: false,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@ import { ActivityView, ViewsStats } from '@kbn/content-management-content-insigh
|
|||
/**
|
||||
* This component is used as an extension for the ContentEditor to render the ActivityView and ViewsStats inside the flyout without depending on them directly
|
||||
*/
|
||||
export const ContentEditorActivityRow: FC<{ item: UserContentCommonSchema }> = ({ item }) => {
|
||||
export const ContentEditorActivityRow: FC<{
|
||||
item: UserContentCommonSchema;
|
||||
entityNamePlural?: string;
|
||||
}> = ({ item, entityNamePlural }) => {
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
|
@ -40,7 +43,7 @@ export const ContentEditorActivityRow: FC<{ item: UserContentCommonSchema }> = (
|
|||
}
|
||||
>
|
||||
<>
|
||||
<ActivityView item={item} />
|
||||
<ActivityView item={item} entityNamePlural={entityNamePlural} />
|
||||
<EuiSpacer size={'s'} />
|
||||
<ViewsStats item={item} />
|
||||
</>
|
||||
|
|
|
@ -113,7 +113,7 @@ export function Table<T extends UserContentCommonSchema>({
|
|||
favoritesEnabled,
|
||||
}: Props<T>) {
|
||||
const euiTheme = useEuiTheme();
|
||||
const { getTagList, isTaggingEnabled } = useServices();
|
||||
const { getTagList, isTaggingEnabled, isKibanaVersioningEnabled } = useServices();
|
||||
|
||||
const renderToolsLeft = useCallback(() => {
|
||||
if (!deleteItems || selectedIds.length === 0) {
|
||||
|
@ -340,6 +340,8 @@ export function Table<T extends UserContentCommonSchema>({
|
|||
}}
|
||||
selectedUsers={tableFilter.createdBy}
|
||||
showNoUserOption={showNoUserOption}
|
||||
isKibanaVersioningEnabled={isKibanaVersioningEnabled}
|
||||
entityNamePlural={entityNamePlural}
|
||||
>
|
||||
<TagFilterContextProvider
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
|
|
|
@ -21,6 +21,8 @@ interface Context {
|
|||
selectedUsers: string[];
|
||||
allUsers: string[];
|
||||
showNoUserOption: boolean;
|
||||
isKibanaVersioningEnabled: boolean;
|
||||
entityNamePlural: string;
|
||||
}
|
||||
|
||||
const UserFilterContext = React.createContext<Context | null>(null);
|
||||
|
@ -44,7 +46,13 @@ export const UserFilterPanel: FC<{}> = () => {
|
|||
if (!componentContext)
|
||||
throw new Error('UserFilterPanel must be used within a UserFilterContextProvider');
|
||||
|
||||
const { onSelectedUsersChange, selectedUsers, showNoUserOption } = componentContext;
|
||||
const {
|
||||
onSelectedUsersChange,
|
||||
selectedUsers,
|
||||
showNoUserOption,
|
||||
isKibanaVersioningEnabled,
|
||||
entityNamePlural,
|
||||
} = componentContext;
|
||||
|
||||
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
@ -126,7 +134,12 @@ export const UserFilterPanel: FC<{}> = () => {
|
|||
id="contentManagement.tableList.listing.userFilter.emptyMessage"
|
||||
defaultMessage="None of the dashboards have creators"
|
||||
/>
|
||||
{<NoCreatorTip />}
|
||||
{
|
||||
<NoCreatorTip
|
||||
includeVersionTip={isKibanaVersioningEnabled}
|
||||
entityNamePlural={entityNamePlural}
|
||||
/>
|
||||
}
|
||||
</p>
|
||||
),
|
||||
nullOptionLabel: i18n.translate(
|
||||
|
@ -136,7 +149,12 @@ export const UserFilterPanel: FC<{}> = () => {
|
|||
}
|
||||
),
|
||||
nullOptionProps: {
|
||||
append: <NoCreatorTip />,
|
||||
append: (
|
||||
<NoCreatorTip
|
||||
includeVersionTip={isKibanaVersioningEnabled}
|
||||
entityNamePlural={entityNamePlural}
|
||||
/>
|
||||
),
|
||||
},
|
||||
clearButtonLabel: (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -76,6 +76,7 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
|
|||
getTagIdsFromReferences: () => [],
|
||||
isTaggingEnabled: () => true,
|
||||
isFavoritesEnabled: () => false,
|
||||
isKibanaVersioningEnabled: false,
|
||||
...params,
|
||||
};
|
||||
|
||||
|
|
|
@ -79,6 +79,9 @@ export interface Services {
|
|||
/** Handler to return the url to navigate to the kibana tags management */
|
||||
getTagManagementUrl: () => string;
|
||||
getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[];
|
||||
/** Whether versioning is enabled for the current kibana instance. (aka is Serverless)
|
||||
This is used to determine if we should show the version mentions in the help text.*/
|
||||
isKibanaVersioningEnabled: boolean;
|
||||
}
|
||||
|
||||
const TableListViewContext = React.createContext<Services | null>(null);
|
||||
|
@ -185,6 +188,12 @@ export interface TableListViewKibanaDependencies {
|
|||
* Content insights client to enable content insights features.
|
||||
*/
|
||||
contentInsightsClient?: ContentInsightsClientPublic;
|
||||
|
||||
/**
|
||||
* Flag to indicate if Kibana versioning is enabled. (aka not Serverless)
|
||||
* Used to determine if we should show the version mentions in the help text.
|
||||
*/
|
||||
isKibanaVersioningEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,7 +260,10 @@ export const TableListViewKibanaProvider: FC<
|
|||
<RedirectAppLinksKibanaProvider coreStart={core}>
|
||||
<UserProfilesKibanaProvider core={core}>
|
||||
<ContentEditorKibanaProvider core={core} savedObjectsTagging={savedObjectsTagging}>
|
||||
<ContentInsightsProvider contentInsightsClient={services.contentInsightsClient}>
|
||||
<ContentInsightsProvider
|
||||
contentInsightsClient={services.contentInsightsClient}
|
||||
isKibanaVersioningEnabled={services.isKibanaVersioningEnabled}
|
||||
>
|
||||
<FavoritesContextProvider
|
||||
favoritesClient={services.favorites}
|
||||
notifyError={(title, text) => {
|
||||
|
@ -282,6 +294,7 @@ export const TableListViewKibanaProvider: FC<
|
|||
itemHasTags={itemHasTags}
|
||||
getTagIdsFromReferences={getTagIdsFromReferences}
|
||||
getTagManagementUrl={() => core.http.basePath.prepend(TAG_MANAGEMENT_APP_URL)}
|
||||
isKibanaVersioningEnabled={services.isKibanaVersioningEnabled ?? false}
|
||||
>
|
||||
{children}
|
||||
</TableListViewProvider>
|
||||
|
|
|
@ -376,6 +376,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
DateFormatterComp,
|
||||
getTagList,
|
||||
isFavoritesEnabled,
|
||||
isKibanaVersioningEnabled,
|
||||
} = useServices();
|
||||
|
||||
const openContentEditor = useOpenContentEditor();
|
||||
|
@ -578,7 +579,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
appendRows: contentInsightsServices && (
|
||||
// have to "REWRAP" in the provider here because it will be rendered in a different context
|
||||
<ContentInsightsProvider {...contentInsightsServices}>
|
||||
<ContentEditorActivityRow item={item} />
|
||||
<ContentEditorActivityRow item={item} entityNamePlural={entityNamePlural} />
|
||||
</ContentInsightsProvider>
|
||||
),
|
||||
});
|
||||
|
@ -591,6 +592,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
tableItemsRowActions,
|
||||
fetchItems,
|
||||
contentInsightsServices,
|
||||
entityNamePlural,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -646,7 +648,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
) : record.managed ? (
|
||||
<ManagedAvatarTip entityName={entityName} />
|
||||
) : (
|
||||
<NoCreatorTip iconType={'minus'} />
|
||||
<NoCreatorTip iconType={'minus'} includeVersionTip={isKibanaVersioningEnabled} />
|
||||
),
|
||||
sortable:
|
||||
false /* createdBy column is not sortable because it doesn't make sense to sort by id*/,
|
||||
|
@ -753,6 +755,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
inspectItem,
|
||||
entityName,
|
||||
isFavoritesEnabled,
|
||||
isKibanaVersioningEnabled,
|
||||
]);
|
||||
|
||||
const itemsById = useMemo(() => {
|
||||
|
|
|
@ -7,29 +7,67 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiIconTip, IconType } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const NoCreatorTip = (props: { iconType?: IconType }) => (
|
||||
const fallbackEntityNamePlural = i18n.translate(
|
||||
'contentManagement.userProfiles.fallbackEntityNamePlural',
|
||||
{ defaultMessage: 'objects' }
|
||||
);
|
||||
|
||||
export const NoCreatorTip = (props: {
|
||||
iconType?: IconType;
|
||||
includeVersionTip?: boolean;
|
||||
entityNamePlural?: string;
|
||||
}) => (
|
||||
<NoUsersTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noCreatorTip"
|
||||
defaultMessage="Creators are assigned when objects are created"
|
||||
/>
|
||||
props.includeVersionTip ? (
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noCreatorTipWithVersion"
|
||||
defaultMessage="Created by is set when {entityNamePlural} are created by users (not by API) starting from version {version}"
|
||||
values={{
|
||||
version: '8.14',
|
||||
entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noCreatorTip"
|
||||
defaultMessage="Created by is set when {entityNamePlural} are created by users (not by API)"
|
||||
values={{ entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const NoUpdaterTip = (props: { iconType?: string }) => (
|
||||
export const NoUpdaterTip = (props: {
|
||||
iconType?: string;
|
||||
includeVersionTip?: boolean;
|
||||
entityNamePlural?: string;
|
||||
}) => (
|
||||
<NoUsersTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noUpdaterTip"
|
||||
defaultMessage="Updated by is set when objects are updated"
|
||||
/>
|
||||
props.includeVersionTip ? (
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noUpdaterTipWithVersion"
|
||||
defaultMessage="Updated by is set when {entityNamePlural} are updated by users (not by API) starting from version {version}"
|
||||
values={{
|
||||
version: '8.15',
|
||||
entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noUpdaterTip"
|
||||
defaultMessage="Updated by is set when {entityNamePlural} are created by users (not by API)"
|
||||
values={{ entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID } from '../dashboard_constants';
|
|||
import {
|
||||
coreServices,
|
||||
savedObjectsTaggingService,
|
||||
serverlessService,
|
||||
usageCollectionService,
|
||||
} from '../services/kibana_services';
|
||||
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
|
||||
|
@ -65,6 +66,7 @@ export const DashboardListing = ({
|
|||
FormattedRelative,
|
||||
favorites: dashboardFavoritesClient,
|
||||
contentInsightsClient,
|
||||
isKibanaVersioningEnabled: !serverlessService,
|
||||
}}
|
||||
>
|
||||
<TableListView<DashboardSavedObjectUserContent> {...tableListViewTableProps}>
|
||||
|
|
|
@ -16,7 +16,11 @@ import {
|
|||
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
|
||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { coreServices, savedObjectsTaggingService } from '../services/kibana_services';
|
||||
import {
|
||||
coreServices,
|
||||
savedObjectsTaggingService,
|
||||
serverlessService,
|
||||
} from '../services/kibana_services';
|
||||
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
|
||||
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
|
||||
import { DashboardListingProps, DashboardSavedObjectUserContent } from './types';
|
||||
|
@ -57,6 +61,7 @@ export const DashboardListingTable = ({
|
|||
savedObjectsTagging={savedObjectsTaggingService?.getTaggingApi()}
|
||||
FormattedRelative={FormattedRelative}
|
||||
contentInsightsClient={contentInsightsClient}
|
||||
isKibanaVersioningEnabled={!serverlessService}
|
||||
>
|
||||
<>
|
||||
<DashboardUnsavedListing
|
||||
|
|
|
@ -623,8 +623,6 @@
|
|||
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "Géré",
|
||||
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "Cette entité {entityName} est créée et gérée par Elastic. Clonez-le pour effectuer des modifications.",
|
||||
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "objet",
|
||||
"contentManagement.userProfiles.noCreatorTip": "Les créateurs sont assignés lors de la création des objets",
|
||||
"contentManagement.userProfiles.noUpdaterTip": "Le champ Mis à jour par est défini lors de la mise à jour des objets",
|
||||
"controls.blockingError": "Une erreur s'est produite lors du chargement de ce contrôle.",
|
||||
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "Une usine de contrôle pour le type : {key} est déjà enregistrée.",
|
||||
"controls.controlFactoryRegistry.factoryNotFoundError": "Aucune usine de contrôle n'a été trouvée pour le type : {key}",
|
||||
|
|
|
@ -625,8 +625,6 @@
|
|||
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "管理中",
|
||||
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "この{entityName}はElasticによって作成され、管理されています。変更するには、複製してください。",
|
||||
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "オブジェクト",
|
||||
"contentManagement.userProfiles.noCreatorTip": "作成担当は、オブジェクトが作成されるときに割り当てられます",
|
||||
"contentManagement.userProfiles.noUpdaterTip": "更新は、オブジェクトが更新されるときに割り当てられます",
|
||||
"controls.blockingError": "このコントロールの読み込みエラーが発生しました。",
|
||||
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "タイプ\"{key}\"のコントロールファクトリはすでに登録されています。",
|
||||
"controls.controlFactoryRegistry.factoryNotFoundError": "タイプ\"{key}\"のコントロールファクトリが見つかりません",
|
||||
|
|
|
@ -647,8 +647,6 @@
|
|||
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "托管",
|
||||
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "此 {entityName} 由 Elastic 创建和管理。进行克隆以做出更改。",
|
||||
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "对象",
|
||||
"contentManagement.userProfiles.noCreatorTip": "将在创建对象时分配创建者",
|
||||
"contentManagement.userProfiles.noUpdaterTip": "将在更新对象时设置更新者",
|
||||
"controls.blockingError": "加载此控件时出错。",
|
||||
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "已注册类型为 {key} 的控制工厂。",
|
||||
"controls.controlFactoryRegistry.factoryNotFoundError": "未找到类型为 {key} 的控制工厂",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue