mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Connectors] Detect deploying agentless infra state (#205395)
## Summary Let’s start with simple logic to detect the state where we’re waiting for the agentless infrastructure to be provisioned. In a recent change (https://github.com/elastic/connectors/pull/3014), connectors now send a heartbeat immediately upon the framework’s startup. Therefore, we can use the “last seen” timestamp (populated on heartbeat) to detect when the infrastructure has started. If we find this logic insufficient to cover all cases, we can adapt the ConnectorViewLogic to also call Fleet APIs for more accurate missing/ready agentless host detection . Let’s keep it simple for now and iterate as needed. ### Changes - Add logic in ConnectorViewLogic to compute the isWaitingOnAgentlessDeployment state using the last_seen property. - In the connector creation flow, add a “Provisioning Infrastructure” banner and disable the “Next Step” button if we’re in this state. - In the connector overview, add a warning banner if the infrastructure is not provisioned. - Add a few additional fixes in the “Create Connector” form. ### Screenshots - Create connector form, banner + next button disabled <img width="1265" alt="Screenshot 2025-01-02 at 15 14 56" src="https://github.com/user-attachments/assets/32b224ae-8008-429e-a940-39bf038a03dc" /> - Connector overview banner <img width="1280" alt="Screenshot 2025-01-02 at 15 15 32" src="https://github.com/user-attachments/assets/d29bccf6-b6ed-48ab-9b58-076c3962ad36" /> - Form is non-editable after connector doc is created, don't show API key related info for elastic-managed connector <img width="1296" alt="Screenshot 2025-01-02 at 15 25 56" src="https://github.com/user-attachments/assets/766a1fd7-cef3-4437-8140-4559f8cf5de1" /> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
18fd1713b8
commit
23c28eb712
7 changed files with 211 additions and 94 deletions
|
@ -97,6 +97,9 @@ export const GeneratedConfigFields: React.FC<GeneratedConfigFieldsProps> = ({
|
|||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const showApiKeyInfoForSelfManagedConnector = !connector.is_native;
|
||||
const showApiKeyBanner = showApiKeyInfoForSelfManagedConnector && apiKey?.encoded;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModalVisible && <ConfirmModal onCancel={onCancel} onConfirm={onConfirm} />}
|
||||
|
@ -193,96 +196,99 @@ export const GeneratedConfigFields: React.FC<GeneratedConfigFieldsProps> = ({
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs">
|
||||
{showApiKeyInfoForSelfManagedConnector && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="check" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.apiKeyCreatedFlexItemLabel',
|
||||
{ defaultMessage: 'API key created' }
|
||||
)}
|
||||
{apiKey?.encoded && ` *`}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="check" />
|
||||
<EuiLink
|
||||
data-test-subj="enterpriseSearchConnectorDeploymentLink"
|
||||
href={generateEncodedPath(MANAGE_API_KEYS_URL, {})}
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
{apiKey?.name}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.apiKeyCreatedFlexItemLabel',
|
||||
{ defaultMessage: 'API key created' }
|
||||
)}
|
||||
{apiKey?.encoded && ` *`}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="enterpriseSearchConnectorDeploymentLink"
|
||||
href={generateEncodedPath(MANAGE_API_KEYS_URL, {})}
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
{apiKey?.name}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
>
|
||||
{apiKey?.encoded ? (
|
||||
<EuiFlexItem>
|
||||
<EuiCopy textToCopy={apiKey?.encoded}>
|
||||
{(copy) => (
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiCode>{apiKey?.encoded}</EuiCode>
|
||||
</EuiFlexItem>
|
||||
{generateApiKey && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
|
||||
size="xs"
|
||||
iconType="refresh"
|
||||
isLoading={isGenerateLoading}
|
||||
onClick={refreshButtonClick}
|
||||
disabled={!connector.index_name}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.refreshAPIKey',
|
||||
{ defaultMessage: 'Refresh an Elasticsearch API key' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
data-test-subj="enterpriseSearchConnectorDeploymentButton"
|
||||
iconType="copyClipboard"
|
||||
onClick={copy}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.copyIndexName',
|
||||
{ defaultMessage: 'Copy index name' }
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
>
|
||||
{apiKey?.encoded ? (
|
||||
<EuiFlexItem>
|
||||
<EuiCopy textToCopy={apiKey?.encoded}>
|
||||
{(copy) => (
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiCode>{apiKey?.encoded}</EuiCode>
|
||||
</EuiFlexItem>
|
||||
{generateApiKey && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
|
||||
size="xs"
|
||||
iconType="refresh"
|
||||
isLoading={isGenerateLoading}
|
||||
onClick={refreshButtonClick}
|
||||
disabled={!connector.index_name}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.refreshAPIKey',
|
||||
{ defaultMessage: 'Refresh an Elasticsearch API key' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
generateApiKey && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
|
||||
size="xs"
|
||||
iconType="refresh"
|
||||
isLoading={isGenerateLoading}
|
||||
onClick={refreshButtonClick}
|
||||
disabled={!connector.index_name}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
data-test-subj="enterpriseSearchConnectorDeploymentButton"
|
||||
iconType="copyClipboard"
|
||||
onClick={copy}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.connectorDeployment.copyIndexName',
|
||||
{ defaultMessage: 'Copy index name' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
generateApiKey && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
|
||||
size="xs"
|
||||
iconType="refresh"
|
||||
isLoading={isGenerateLoading}
|
||||
onClick={refreshButtonClick}
|
||||
disabled={!connector.index_name}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
|
||||
{apiKey?.encoded && (
|
||||
{showApiKeyBanner && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
|
|
|
@ -34,7 +34,11 @@ import {
|
|||
hasDocumentLevelSecurityFeature,
|
||||
hasIncrementalSyncFeature,
|
||||
} from '../../utils/connector_helpers';
|
||||
import { getConnectorLastSeenError, isLastSeenOld } from '../../utils/connector_status_helpers';
|
||||
import {
|
||||
getConnectorLastSeenError,
|
||||
hasConnectorBeenSeenRecently,
|
||||
isLastSeenOld,
|
||||
} from '../../utils/connector_status_helpers';
|
||||
|
||||
import {
|
||||
ConnectorNameAndDescriptionActions,
|
||||
|
@ -82,6 +86,7 @@ export interface ConnectorViewValues {
|
|||
isCanceling: boolean;
|
||||
isHiddenIndex: boolean;
|
||||
isLoading: boolean;
|
||||
isWaitingOnAgentlessDeployment: boolean;
|
||||
lastUpdated: string | null;
|
||||
pipelineData: IngestPipelineParams | undefined;
|
||||
recheckIndexLoading: boolean;
|
||||
|
@ -206,6 +211,7 @@ export const ConnectorViewLogic = kea<MakeLogicType<ConnectorViewValues, Connect
|
|||
() => [selectors.hasAdvancedFilteringFeature, selectors.hasBasicFilteringFeature],
|
||||
(advancedFeature: boolean, basicFeature: boolean) => advancedFeature || basicFeature,
|
||||
],
|
||||
|
||||
hasIncrementalSyncFeature: [
|
||||
() => [selectors.connector],
|
||||
(connector?: Connector) => hasIncrementalSyncFeature(connector),
|
||||
|
@ -231,6 +237,14 @@ export const ConnectorViewLogic = kea<MakeLogicType<ConnectorViewValues, Connect
|
|||
[Status.IDLE && Status.LOADING].includes(fetchConnectorApiStatus) ||
|
||||
(index && [Status.IDLE && Status.LOADING].includes(fetchIndexApiStatus)),
|
||||
],
|
||||
isWaitingOnAgentlessDeployment: [
|
||||
() => [selectors.connector],
|
||||
(connector: Connector) => {
|
||||
if (!connector || !connector.is_native) return false;
|
||||
|
||||
return !hasConnectorBeenSeenRecently(connector);
|
||||
},
|
||||
],
|
||||
pipelineData: [
|
||||
() => [selectors.connector],
|
||||
(connector: Connector | undefined) => connector?.pipeline ?? undefined,
|
||||
|
|
|
@ -9,7 +9,17 @@ import React from 'react';
|
|||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiCode, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -38,7 +48,7 @@ import { ConnectorViewLogic } from './connector_view_logic';
|
|||
|
||||
export const ConnectorDetailOverview: React.FC = () => {
|
||||
const { indexData } = useValues(IndexViewLogic);
|
||||
const { connector, error } = useValues(ConnectorViewLogic);
|
||||
const { connector, error, isWaitingOnAgentlessDeployment } = useValues(ConnectorViewLogic);
|
||||
const { isCloud } = useValues(KibanaLogic);
|
||||
const { showModal } = useActions(ConvertConnectorLogic);
|
||||
const { isModalVisible } = useValues(ConvertConnectorLogic);
|
||||
|
@ -73,6 +83,39 @@ export const ConnectorDetailOverview: React.FC = () => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
{isWaitingOnAgentlessDeployment && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.connectors.overview.agentlessDeploymentNotReadyCallOut.title',
|
||||
{
|
||||
defaultMessage: 'Provisioning infrastructure',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.connectors.overview.agentlessDeploymentNotReadyCallOut.description',
|
||||
{
|
||||
defaultMessage: 'Setting up the agentless infrastructure to run the connector.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{error && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
|
|
|
@ -37,12 +37,16 @@ import { SelfManagePreference } from '../create_connector';
|
|||
|
||||
interface ChooseConnectorSelectableProps {
|
||||
selfManaged: SelfManagePreference;
|
||||
disabled?: boolean;
|
||||
}
|
||||
interface OptionData {
|
||||
secondaryContent?: string;
|
||||
}
|
||||
|
||||
export const ChooseConnector: React.FC<ChooseConnectorSelectableProps> = ({ selfManaged }) => {
|
||||
export const ChooseConnector: React.FC<ChooseConnectorSelectableProps> = ({
|
||||
selfManaged,
|
||||
disabled,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [selectedOption, setSelectedOption] = useState<Array<EuiComboBoxOptionOption<OptionData>>>(
|
||||
[]
|
||||
|
@ -142,6 +146,7 @@ export const ChooseConnector: React.FC<ChooseConnectorSelectableProps> = ({ self
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
isDisabled={disabled}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.euiComboBox.accessibleScreenReaderLabelLabel',
|
||||
{ defaultMessage: 'Select a data source for your connector to use.' }
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
EuiText,
|
||||
EuiButton,
|
||||
EuiProgress,
|
||||
EuiCallOut,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -38,7 +40,7 @@ interface ConfigurationStepProps {
|
|||
}
|
||||
|
||||
export const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ title, setCurrentStep }) => {
|
||||
const { connector } = useValues(ConnectorViewLogic);
|
||||
const { connector, isWaitingOnAgentlessDeployment } = useValues(ConnectorViewLogic);
|
||||
const { updateConnectorConfiguration } = useActions(ConnectorViewLogic);
|
||||
const { setFormDirty } = useActions(NewConnectorLogic);
|
||||
const { overlays } = useKibana().services;
|
||||
|
@ -46,10 +48,12 @@ export const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ title, set
|
|||
const { status } = useValues(ConnectorConfigurationApiLogic);
|
||||
const isSyncing = false;
|
||||
|
||||
const isNextStepEnabled =
|
||||
const isConnectorConfigured =
|
||||
connector?.status === ConnectorStatus.CONNECTED ||
|
||||
connector?.status === ConnectorStatus.CONFIGURED;
|
||||
|
||||
const isNextStepEnabled = !isWaitingOnAgentlessDeployment && isConnectorConfigured;
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
|
@ -64,6 +68,37 @@ export const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ title, set
|
|||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="m" direction="column">
|
||||
{isWaitingOnAgentlessDeployment && (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.createConnector.configurationStep.agentlessDeploymentNotReadyCallOut.title',
|
||||
{
|
||||
defaultMessage: 'Provisioning infrastructure',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.createConnector.configurationStep.agentlessDeploymentNotReadyCallOut.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Setting up the agentless infrastructure to run the connector. This process may take up to one minute.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l" style={{ position: 'relative' }}>
|
||||
<EuiTitle size="m">
|
||||
|
@ -114,6 +149,7 @@ export const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ title, set
|
|||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton
|
||||
disabled={!isNextStepEnabled}
|
||||
data-test-subj="enterpriseSearchStartStepGenerateConfigurationButton"
|
||||
onClick={async () => {
|
||||
if (isFormEditing) {
|
||||
|
|
|
@ -107,7 +107,7 @@ export const StartStep: React.FC<StartStepProps> = ({
|
|||
{ defaultMessage: 'Connector' }
|
||||
)}
|
||||
>
|
||||
<ChooseConnector selfManaged={selfManagePreference} />
|
||||
<ChooseConnector selfManaged={selfManagePreference} disabled={!!connector} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5}>
|
||||
|
@ -138,7 +138,7 @@ export const StartStep: React.FC<StartStepProps> = ({
|
|||
<EuiFieldText
|
||||
data-test-subj="enterpriseSearchStartStepFieldText"
|
||||
fullWidth
|
||||
name="first"
|
||||
name="connectorName"
|
||||
value={rawName}
|
||||
onChange={handleNameChange}
|
||||
disabled={!!connector}
|
||||
|
@ -173,9 +173,10 @@ export const StartStep: React.FC<StartStepProps> = ({
|
|||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
disabled={!!connector}
|
||||
data-test-subj="enterpriseSearchStartStepFieldText"
|
||||
fullWidth
|
||||
name="first"
|
||||
name="connectorDescription"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -26,6 +26,18 @@ export const getConnectorLastSeenError = (connector: Connector): string => {
|
|||
);
|
||||
};
|
||||
|
||||
// Determines if the connector has been seen recently.
|
||||
// Note: The default heartbeat interval for the connector service is every 5 minutes.
|
||||
// This is configured using the `service.heartbeat` key in:
|
||||
// https://github.com/elastic/connectors/blob/main/connectors/config.py
|
||||
export const hasConnectorBeenSeenRecently = (
|
||||
connector: Connector,
|
||||
timeWindowMinutes: number = 10
|
||||
): boolean =>
|
||||
connector.last_seen
|
||||
? moment(connector.last_seen).isSameOrAfter(moment().subtract(timeWindowMinutes, 'minutes'))
|
||||
: false;
|
||||
|
||||
const incompleteText = i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndices.ingestionStatus.incomplete.label',
|
||||
{ defaultMessage: 'Incomplete' }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue