Skip to content

Commit

Permalink
[Files] Create files example plugin (elastic#139019)
Browse files Browse the repository at this point in the history
* created files example stub

* implementing whimsical design

* added base64d image

* updated files client and example plugin to work end-to-end with browser

* added file deletion funcitonality

* added codeowners entry

* refactor downloadSrc to getDownloadHref

* react-query -> @tanstack/react-query

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
2 people authored and Mpdreamz committed Sep 6, 2022
1 parent 5df1340 commit 2c9c321
Show file tree
Hide file tree
Showing 24 changed files with 620 additions and 10 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services
/src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services
x-pack/plugins/files @elastic/kibana-app-services
x-pack/examples/files_example @elastic/kibana-app-services

### Observability Plugins

Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@
"@kbn/embedded-lens-example-plugin/*": ["x-pack/examples/embedded_lens_example/*"],
"@kbn/exploratory-view-example-plugin": ["x-pack/examples/exploratory_view_example"],
"@kbn/exploratory-view-example-plugin/*": ["x-pack/examples/exploratory_view_example/*"],
"@kbn/files-example-plugin": ["x-pack/examples/files_example"],
"@kbn/files-example-plugin/*": ["x-pack/examples/files_example/*"],
"@kbn/reporting-example-plugin": ["x-pack/examples/reporting_example"],
"@kbn/reporting-example-plugin/*": ["x-pack/examples/reporting_example/*"],
"@kbn/screenshotting-example-plugin": ["x-pack/examples/screenshotting_example"],
Expand Down
7 changes: 7 additions & 0 deletions x-pack/examples/files_example/.i18nrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"prefix": "filesExample",
"paths": {
"filesExample": "."
},
"translations": ["translations/ja-JP.json"]
}
4 changes: 4 additions & 0 deletions x-pack/examples/files_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# filesExample

An example plugin that integrates with the Kibana "files" plugin.

28 changes: 28 additions & 0 deletions x-pack/examples/files_example/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { FileKind } from '@kbn/files-plugin/common';

export const PLUGIN_ID = 'filesExample';
export const PLUGIN_NAME = 'filesExample';

const httpTags = {
tags: [`access:${PLUGIN_ID}`],
};

export const exampleFileKind: FileKind = {
id: 'filesExample',
http: {
create: httpTags,
delete: httpTags,
download: httpTags,
getById: httpTags,
list: httpTags,
share: httpTags,
update: httpTags,
},
};
14 changes: 14 additions & 0 deletions x-pack/examples/files_example/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": "filesExample",
"version": "1.0.0",
"kibanaVersion": "kibana",
"owner": {
"name": "kibana-app-services",
"githubTeam": "kibana-app-services"
},
"description": "Example plugin integrating with files plugin",
"server": true,
"ui": true,
"requiredPlugins": ["files"],
"optionalPlugins": []
}
30 changes: 30 additions & 0 deletions x-pack/examples/files_example/public/application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { AppPluginStartDependencies } from './types';
import { FilesExampleApp } from './components/app';

const queryClient = new QueryClient();

export const renderApp = (
{ notifications }: CoreStart,
{ files }: AppPluginStartDependencies,
{ element }: AppMountParameters
) => {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<FilesExampleApp files={files} notifications={notifications} />
</QueryClientProvider>,
element
);

return () => ReactDOM.unmountComponentAtNode(element);
};

Large diffs are not rendered by default.

169 changes: 169 additions & 0 deletions x-pack/examples/files_example/public/components/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { FileJSON } from '@kbn/files-plugin/common';
import type { FilesClientResponses } from '@kbn/files-plugin/public';

const names = ['foo', 'bar', 'baz'];

import {
EuiPageTemplate,
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiButton,
EuiIcon,
EuiButtonIcon,
EuiLink,
} from '@elastic/eui';

import { CoreStart } from '@kbn/core/public';
import { DetailsFlyout } from './details_flyout';
import type { FileClients } from '../types';
import { ConfirmButtonIcon } from './confirm_button';
// @ts-ignore
import imageBase64 from '!!raw-loader!../assets/image.png.base64';

interface FilesExampleAppDeps {
files: FileClients;
notifications: CoreStart['notifications'];
}

type ListResponse = FilesClientResponses['list'];

export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) => {
const { data, isLoading, error, refetch } = useQuery<ListResponse>(['files'], () =>
files.example.list()
);
const [isUploadingImage, setIsUploadingImage] = useState(false);
const [isDeletingFile, setIsDeletingFile] = useState(false);
const [selectedItem, setSelectedItem] = useState<undefined | FileJSON>();

const uploadImage = async () => {
try {
setIsUploadingImage(true);
const { file } = await files.example.create({
name: names[Math.floor(Math.random() * names.length)],
alt: 'My image',
meta: { myValue: 'test' },
mimeType: 'image/png',
});
await refetch();
const blob = new Blob([Uint8Array.from(atob(imageBase64), (c) => c.charCodeAt(0))], {
type: 'image/png',
});
await files.example.upload({ id: file.id, body: blob });
await refetch();
notifications.toasts.addSuccess('Sucessfully uploaded image');
} finally {
setIsUploadingImage(false);
}
};

const renderToolsRight = () => {
return [
<EuiButton
onClick={uploadImage}
isDisabled={isUploadingImage || isLoading || isDeletingFile}
isLoading={isUploadingImage}
iconType="exportAction"
>
Upload image
</EuiButton>,
];
};

const items = [...(data?.files ?? [])].reverse();

const columns: EuiInMemoryTableProps<FileJSON>['columns'] = [
{
field: 'name',
name: 'Name',
render: (name, item) => <EuiLink onClick={() => setSelectedItem(item)}>{name}</EuiLink>,
},
{
field: 'status',
name: 'Status',
render: (status: FileJSON['status']) =>
status === 'READY' ? (
<EuiIcon color="success" type="checkInCircleFilled" aria-label={status} />
) : status === 'AWAITING_UPLOAD' ? (
<EuiIcon type="clock" aria-label={status} />
) : (
<EuiIcon color="danger" type="alert" arial-label={status} />
),
},
{
name: 'Actions',
actions: [
{
name: 'View',
description: 'View file',
isPrimary: true,
render: (item) => (
<EuiButtonIcon
aria-label="View file details"
iconType="eye"
onClick={() => setSelectedItem(item)}
/>
),
},
{
name: 'Delete',
description: 'Delete this file',
render: (item) => (
<ConfirmButtonIcon
disabled={isDeletingFile}
label="Delete this file"
confirmationText="Are you sure you want to delete this file?"
onConfirm={async () => {
try {
setIsDeletingFile(true);
await files.example.delete({ id: item.id });
await refetch();
} finally {
setIsDeletingFile(false);
}
}}
/>
),
},
],
},
];

return (
<>
<EuiPageTemplate
pageHeader={{
pageTitle: 'Files example',
}}
>
<EuiInMemoryTable
columns={columns}
items={items}
itemId="id"
loading={isLoading || isDeletingFile}
error={error ? JSON.stringify(error) : undefined}
sorting
search={{
toolsRight: renderToolsRight(),
}}
pagination
/>
</EuiPageTemplate>
{selectedItem && (
<DetailsFlyout
files={files}
file={selectedItem}
onDismiss={() => setSelectedItem(undefined)}
/>
)}
</>
);
};
49 changes: 49 additions & 0 deletions x-pack/examples/files_example/public/components/confirm_button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useState, FunctionComponent } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';

interface Props {
label: string;
confirmationText: string;
onConfirm: () => void;
disabled: boolean;
}

export const ConfirmButtonIcon: FunctionComponent<Props> = ({
label,
confirmationText,
onConfirm,
disabled,
}) => {
const [showConfirm, setShowConfirm] = useState(false);

return showConfirm ? (
<EuiToolTip content={confirmationText}>
<EuiButtonIcon
disabled={disabled}
aria-label={confirmationText}
color="warning"
iconType="alert"
onClick={onConfirm}
/>
</EuiToolTip>
) : (
<EuiButtonIcon
aria-label={label}
disabled={disabled}
color="danger"
iconType="trash"
onClick={(e: { stopPropagation: () => void }) => {
e.stopPropagation();
setShowConfirm(true);
setTimeout(() => setShowConfirm(false), 3000);
}}
/>
);
};
Loading

0 comments on commit 2c9c321

Please sign in to comment.