Skip to content

Commit

Permalink
Merge branch 'main' into reduce-obs-knowledge-code-ownership
Browse files Browse the repository at this point in the history
  • Loading branch information
miltonhultgren authored Nov 27, 2024
2 parents 7113868 + 048d5eb commit e5c1e42
Show file tree
Hide file tree
Showing 32 changed files with 499 additions and 195 deletions.
2 changes: 2 additions & 0 deletions .buildkite/ftr_oblt_serverless_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ enabled:
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group4.ts
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group5.ts
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group6.ts
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group7.ts
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group8.ts
- x-pack/test_serverless/functional/test_suites/observability/config.screenshots.ts
# serverless config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
Expand Down
2 changes: 2 additions & 0 deletions .buildkite/ftr_search_serverless_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ enabled:
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group4.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group5.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group6.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group7.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group8.ts
# serverless config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts
2 changes: 2 additions & 0 deletions .buildkite/ftr_security_serverless_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ enabled:
- x-pack/test_serverless/functional/test_suites/security/common_configs/config.group4.ts
- x-pack/test_serverless/functional/test_suites/security/common_configs/config.group5.ts
- x-pack/test_serverless/functional/test_suites/security/common_configs/config.group6.ts
- x-pack/test_serverless/functional/test_suites/security/common_configs/config.group7.ts
- x-pack/test_serverless/functional/test_suites/security/common_configs/config.group8.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/trial_license_complete_tier/configs/serverless.config.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ spec:
default_branch: main
repository: elastic/kibana
pipeline_file: .buildkite/scripts/pipelines/trigger_version_dependent_jobs/pipeline.sh
skip_intermediate_builds: false
provider_settings:
prefix_pull_request_fork_branch_names: false
skip_pull_request_builds_for_existing_commits: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,31 @@ import { ApiKeyFlyoutWrapper } from './api_key_flyout_wrapper';
import { useSearchApiKey } from '../hooks/use_search_api_key';
import { Status } from '../constants';

const API_KEY_MASK = '•'.repeat(60);

interface ApiKeyFormProps {
hasTitle?: boolean;
}

export const ApiKeyForm: React.FC<ApiKeyFormProps> = ({ hasTitle = true }) => {
const [showFlyout, setShowFlyout] = useState(false);
const { apiKey, status, updateApiKey, toggleApiKeyVisibility, displayedApiKey, apiKeyIsVisible } =
useSearchApiKey();
const { apiKey, status, updateApiKey, toggleApiKeyVisibility } = useSearchApiKey();

const titleLocale = i18n.translate('searchApiKeysComponents.apiKeyForm.title', {
defaultMessage: 'API Key',
});

if (apiKey && displayedApiKey) {
if (apiKey) {
return (
<FormInfoField
label={hasTitle ? titleLocale : undefined}
value={displayedApiKey}
value={status === Status.showPreviewKey ? apiKey : API_KEY_MASK}
copyValue={apiKey}
dataTestSubj="apiKeyFormAPIKey"
copyValueDataTestSubj="APIKeyButtonCopy"
actions={[
<EuiButtonIcon
iconType={apiKeyIsVisible ? 'eyeClosed' : 'eye'}
iconType={status === Status.showPreviewKey ? 'eyeClosed' : 'eye'}
color="text"
onClick={toggleApiKeyVisibility}
data-test-subj="showAPIKeyButton"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { useMutation } from '@tanstack/react-query';
import type { APIKeyCreationResponse } from '@kbn/search-api-keys-server/types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { APIRoutes } from '../types';

export const useCreateApiKey = ({
onSuccess,
onError,
}: {
onSuccess(key: APIKeyCreationResponse): void;
onError(err: XMLHttpRequest): void;
}) => {
const { http } = useKibana().services;
const { mutateAsync: createApiKey } = useMutation<APIKeyCreationResponse | undefined>({
mutationFn: async () => {
try {
if (!http?.post) {
throw new Error('HTTP service is unavailable');
}

return await http.post<APIKeyCreationResponse>(APIRoutes.API_KEYS);
} catch (err) {
onError(err);
}
},
onSuccess: (receivedApiKey) => {
if (receivedApiKey) {
onSuccess(receivedApiKey);
}
},
});

return createApiKey;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { useMutation } from '@tanstack/react-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { APIRoutes } from '../types';

export const useValidateApiKey = (): ((id: string) => Promise<boolean>) => {
const { http } = useKibana().services;
const { mutateAsync: validateApiKey } = useMutation(async (id: string) => {
try {
if (!http?.post) {
throw new Error('HTTP service is unavailable');
}

const response = await http.post<{ isValid: boolean }>(APIRoutes.API_KEY_VALIDITY, {
body: JSON.stringify({ id }),
});

return response.isValid;
} catch (err) {
return false;
}
});

return validateApiKey;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,176 +7,109 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useCallback, useReducer, createContext, useEffect } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { APIKeyCreationResponse } from '@kbn/search-api-keys-server/types';
import { APIRoutes } from '../types';
import React, { useCallback, createContext, useState, useMemo, useRef } from 'react';
import { useCreateApiKey } from '../hooks/use_create_api_key';
import { Status } from '../constants';
import { useValidateApiKey } from '../hooks/use_validate_api_key';

const API_KEY_STORAGE_KEY = 'searchApiKey';
const API_KEY_MASK = '•'.repeat(60);

interface ApiKeyState {
status: Status;
apiKey: string | null;
}

interface APIKeyContext {
displayedApiKey: string | null;
apiKey: string | null;
toggleApiKeyVisibility: () => void;
updateApiKey: ({ id, encoded }: { id: string; encoded: string }) => void;
status: Status;
apiKeyIsVisible: boolean;
initialiseKey: () => void;
}

type Action =
| { type: 'SET_API_KEY'; apiKey: string; status: Status }
| { type: 'SET_STATUS'; status: Status }
| { type: 'CLEAR_API_KEY' }
| { type: 'TOGGLE_API_KEY_VISIBILITY' };

const initialState: ApiKeyState = {
apiKey: null,
status: Status.uninitialized,
};

const reducer = (state: ApiKeyState, action: Action): ApiKeyState => {
switch (action.type) {
case 'SET_API_KEY':
return { ...state, apiKey: action.apiKey, status: action.status };
case 'SET_STATUS':
return { ...state, status: action.status };
case 'TOGGLE_API_KEY_VISIBILITY':
return {
...state,
status:
state.status === Status.showHiddenKey ? Status.showPreviewKey : Status.showHiddenKey,
};
case 'CLEAR_API_KEY':
return { ...state, apiKey: null, status: Status.showCreateButton };
default:
return state;
}
};

export const ApiKeyContext = createContext<APIKeyContext>({
displayedApiKey: null,
apiKey: null,
toggleApiKeyVisibility: () => {},
updateApiKey: () => {},
status: Status.uninitialized,
apiKeyIsVisible: false,
initialiseKey: () => {},
});

export const SearchApiKeyProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const { http } = useKibana().services;
const [state, dispatch] = useReducer(reducer, initialState);

const isInitialising = useRef(false);
const [apiKey, setApiKey] = useState<string | null>(null);
const [status, setStatus] = useState<Status>(Status.uninitialized);
const updateApiKey = useCallback(({ id, encoded }: { id: string; encoded: string }) => {
sessionStorage.setItem(API_KEY_STORAGE_KEY, JSON.stringify({ id, encoded }));
dispatch({ type: 'SET_API_KEY', apiKey: encoded, status: Status.showHiddenKey });
}, []);
const handleShowKeyVisibility = useCallback(() => {
dispatch({ type: 'TOGGLE_API_KEY_VISIBILITY' });
setApiKey(encoded);
setStatus(Status.showHiddenKey);
}, []);
const initialiseKey = useCallback(() => {
dispatch({ type: 'SET_STATUS', status: Status.loading });
const toggleApiKeyVisibility = useCallback(() => {
setStatus((prevStatus) =>
prevStatus === Status.showHiddenKey ? Status.showPreviewKey : Status.showHiddenKey
);
}, []);
const { mutateAsync: validateApiKey } = useMutation(async (id: string) => {
try {
if (!http?.post) {
throw new Error('HTTP service is unavailable');
}

const response = await http.post<{ isValid: boolean }>(APIRoutes.API_KEY_VALIDITY, {
body: JSON.stringify({ id }),
});

return response.isValid;
} catch (err) {
return false;
}
});
const { mutateAsync: createApiKey } = useMutation<APIKeyCreationResponse | undefined>({
mutationFn: async () => {
try {
if (!http?.post) {
throw new Error('HTTP service is unavailable');
}

return await http.post<APIKeyCreationResponse>(APIRoutes.API_KEYS);
} catch (err) {
if (err.response?.status === 400) {
dispatch({ type: 'SET_STATUS', status: Status.showCreateButton });
} else if (err.response?.status === 403) {
dispatch({ type: 'SET_STATUS', status: Status.showUserPrivilegesError });
} else {
throw err;
}
}
},
const validateApiKey = useValidateApiKey();
const createApiKey = useCreateApiKey({
onSuccess: (receivedApiKey) => {
if (receivedApiKey) {
sessionStorage.setItem(
API_KEY_STORAGE_KEY,
JSON.stringify({ id: receivedApiKey.id, encoded: receivedApiKey.encoded })
);
dispatch({
type: 'SET_API_KEY',
apiKey: receivedApiKey.encoded,
status: Status.showHiddenKey,
});
setApiKey(receivedApiKey.encoded);
setStatus(Status.showHiddenKey);
}
},
onError: (err) => {
if (err.response?.status === 400) {
setStatus(Status.showCreateButton);
} else if (err.response?.status === 403) {
setStatus(Status.showUserPrivilegesError);
} else {
throw err;
}
},
});
const initialiseKey = useCallback(async () => {
if (status !== Status.uninitialized || isInitialising.current) {
return;
}

useEffect(() => {
const initialiseApiKey = async () => {
try {
if (state.status === Status.loading) {
const storedKey = sessionStorage.getItem(API_KEY_STORAGE_KEY);
isInitialising.current = true;

try {
setStatus(Status.loading);
const storedKey = sessionStorage.getItem(API_KEY_STORAGE_KEY);

if (storedKey) {
const { id, encoded } = JSON.parse(storedKey);
if (storedKey) {
const { id, encoded } = JSON.parse(storedKey);

if (await validateApiKey(id)) {
dispatch({
type: 'SET_API_KEY',
apiKey: encoded,
status: Status.showHiddenKey,
});
} else {
sessionStorage.removeItem(API_KEY_STORAGE_KEY);
dispatch({
type: 'CLEAR_API_KEY',
});
await createApiKey();
}
} else {
await createApiKey();
}
if (await validateApiKey(id)) {
setApiKey(encoded);
setStatus(Status.showHiddenKey);
} else {
sessionStorage.removeItem(API_KEY_STORAGE_KEY);
setApiKey(null);
setStatus(Status.showCreateButton);
await createApiKey();
}
} catch (e) {
dispatch({ type: 'CLEAR_API_KEY' });
} else {
await createApiKey();
}
};

initialiseApiKey();
}, [state.status, createApiKey, validateApiKey]);

const value: APIKeyContext = {
displayedApiKey: state.status === Status.showPreviewKey ? state.apiKey : API_KEY_MASK,
apiKey: state.apiKey,
toggleApiKeyVisibility: handleShowKeyVisibility,
updateApiKey,
status: state.status,
apiKeyIsVisible: state.status === Status.showPreviewKey,
initialiseKey,
};
} catch (e) {
setApiKey(null);
setStatus(Status.showCreateButton);
} finally {
isInitialising.current = false;
}
}, [status, createApiKey, validateApiKey]);

const value: APIKeyContext = useMemo(
() => ({
apiKey,
toggleApiKeyVisibility,
updateApiKey,
status,
initialiseKey,
}),
[apiKey, status, toggleApiKeyVisibility, updateApiKey, initialiseKey]
);

return <ApiKeyContext.Provider value={value}>{children}</ApiKeyContext.Provider>;
};
Loading

0 comments on commit e5c1e42

Please sign in to comment.