/* * This file is part of Cockpit. * * Copyright (C) 2017 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ import cockpit from "cockpit"; import React from "react"; import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js"; import PropTypes from "prop-types"; import { debounce } from 'throttle-debounce'; const _ = cockpit.gettext; export class FileAutoComplete extends React.Component { constructor(props) { super(props); this.state = { directory: '', // The current directory we list files/dirs from displayFiles: [], isOpen: false, value: this.props.value || null, }; this.typeaheadInputValue = ""; this.allowFilesUpdate = true; this.updateFiles = this.updateFiles.bind(this); this.finishUpdate = this.finishUpdate.bind(this); this.onToggle = this.onToggle.bind(this); this.clearSelection = this.clearSelection.bind(this); this.onCreateOption = this.onCreateOption.bind(this); this.onPathChange = (value) => { if (!value) { this.clearSelection(); return; } this.typeaheadInputValue = value; const cb = (dirPath) => this.updateFiles(dirPath == '' ? '/' : dirPath); let path = value; if (value.lastIndexOf('/') == value.length - 1) path = value.slice(0, value.length - 1); const match = this.state.displayFiles .find(entry => (entry.type == 'directory' && entry.path == path + '/') || (entry.type == 'file' && entry.path == path)); if (match) { // If match file path is a prefix of another file, do not update current directory, // since we cannot tell file/directory user wants to select // https://bugzilla.redhat.com/show_bug.cgi?id=2097662 const isPrefix = this.state.displayFiles.filter(entry => entry.path.startsWith(value)).length > 1; // If the inserted string corresponds to a directory listed in the results // update the current directory and refetch results if (match.type == 'directory' && !isPrefix) cb(match.path); else this.setState({ value: match.path }); } else { // If the inserted string's parent directory is not matching the `directory` // in the state object we need to update the parent directory and recreate the displayFiles const parentDir = value.slice(0, value.lastIndexOf('/')); if (parentDir + '/' != this.state.directory) { return this.updateFiles(parentDir + '/'); } } }; this.debouncedChange = debounce(300, this.onPathChange); this.onPathChange(this.state.value); } componentWillUnmount() { this.allowFilesUpdate = false; } onCreateOption(newValue) { this.setState(prevState => ({ displayFiles: [...prevState.displayFiles, { type: "file", path: newValue }] })); } updateFiles(path) { if (this.state.directory == path) return; const channel = cockpit.channel({ payload: "fslist1", path, superuser: this.props.superuser, watch: false, }); const results = []; channel.addEventListener("ready", () => { this.finishUpdate(results, null, path); }); channel.addEventListener("close", (ev, data) => { this.finishUpdate(results, data.message, path); }); channel.addEventListener("message", (ev, data) => { const item = JSON.parse(data); if (item && item.path && item.event == 'present') { item.path = item.path + (item.type == 'directory' ? '/' : ''); results.push(item); } }); } finishUpdate(results, error, directory) { if (!this.allowFilesUpdate) return; results = results.sort((a, b) => a.path.localeCompare(b.path, { sensitivity: 'base' })); const listItems = results.map(file => ({ type: file.type, path: (directory == '' ? '/' : directory) + file.path })); if (directory) { listItems.unshift({ type: "directory", path: directory }); } if (error || !this.state.value) this.props.onChange('', error); if (!error) this.setState({ displayFiles: listItems, directory }); this.setState({ error, }); } onToggle(_, isOpen) { this.setState({ isOpen }); } clearSelection() { this.typeaheadInputValue = ""; this.updateFiles("/"); this.setState({ value: null, isOpen: false }); this.props.onChange('', null); } render() { const placeholder = this.props.placeholder || _("Path to file"); const selectOptions = this.state.displayFiles .map(option => ); return ( ); } } FileAutoComplete.propTypes = { id: PropTypes.string, placeholder: PropTypes.string, superuser: PropTypes.string, isOptionCreatable: PropTypes.bool, onChange: PropTypes.func, value: PropTypes.string, }; FileAutoComplete.defaultProps = { isOptionCreatable: false, onChange: () => '', };