<commit_before>/*
 *  This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
 *  License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
 */

/*
Datastore (kucalc only!)
*/

import { React, useState, useIsMountedRef } from "../../app-framework";
import { webapp_client } from "../../webapp-client";
import { useProjectState } from "../page/project-state-hook";
import { useProjectHasInternetAccess } from "./has-internet-access-hook";
import { ReloadOutlined, DeleteOutlined } from "@ant-design/icons";
import { PlusCircleOutlined, EditOutlined } from "@ant-design/icons";
import { Button, Table, Typography, Form, Input, Checkbox } from "antd";
import { Space as AntdSpace, Alert, Switch, Popconfirm, Tooltip } from "antd";
import { ErrorDisplay, SettingBox, Space, Icon, Tip, A } from "../../r_misc";
import { unreachable } from "smc-util/misc";
import { DUMMY_SECRET } from "./const";
import { DatastoreConfig as Config } from "./types";

const SECRET_TOOLTIP = `\nSecrets can't be edited. Either keep "${DUMMY_SECRET}" as it is to retain the current value, or enter a new secret to replace the existing one.`;

const DOC = "https://doc.cocalc.com/project-settings.html#datastore";

const RULE_REQUIRED = [
  { required: true, message: "This is a required field." },
];

const RULE_ALPHANUM = [
  RULE_REQUIRED[0],
  {
    pattern: /^[0-9a-z-_.]{1,63}$/,
    message:
      'Must be lowercase alphanumeric and no spaces, i.e. a-z, 0-9, "-", "." and "_" are allowed. Max. 63 characters long.',
  },
];

// convert the configuration from the DB to fields for the table
function raw2configs(raw: { [name: string]: Config }): Config[] {
  const ret: Config[] = [];
  for (const [k, v] of Object.entries(raw)) {
    v.name = k;
    v.key = k; // for antd, to have unique rows
    switch (v.type) {
      case "s3":
        v.about = [`Key ID: ${v.keyid}`, `Bucket: ${v.bucket}`].join("\n");
        break;
      case "gcs":
        v.about = `Bucket: ${v.bucket}`;
        break;
      case "sshfs":
        v.about = [
          `User: ${v.user}`,
          `Host: ${v.host}`,
          `Path: ${v.path ?? `/user/${v.user}`}`,
        ].join("\n");
        break;
      default:
        unreachable(v);
    }
    ret.push(v);
  }
  return ret;
}

const READONLY_DEFAULT = false;

interface Props {
  project_id: string;
}

export const Datastore: React.FC<Props> = React.memo((props: Props) => {
  const { project_id } = props;
  const state = useProjectState(project_id);
  const has_internet = useProjectHasInternetAccess(project_id);
  const is_running = state.get("state") === "running";
  const [needs_restart, set_needs_restart] = useState<boolean>(false);
  const [loading, set_loading] = useState<boolean>(false);
  const [error, set_error] = useState<string>("");
  const [new_config, set_new_config] = useState<Config | null>(null);
  const [show_help, set_show_help] = useState<boolean>(false);
  const editing = new_config != null;
  const is_mounted_ref = useIsMountedRef();
  const [configs, set_configs] = useState<Config[]>([]);
  const [form_readonly, set_form_readonly] = useState<boolean>(
    READONLY_DEFAULT
  );

  const [form_gcs] = Form.useForm();
  const [form_s3] = Form.useForm();
  const [form_sshfs] = Form.useForm();

  React.useEffect(() => {
    // if there is a change to that project running again, we clear the restart warning
    if (is_running && needs_restart) set_needs_restart(false);
  }, [is_running]);

  async function add(type: Config["type"]): Promise<void> {
    const common = { name: "", secret: "" };
    switch (type) {
      case "s3":
        set_new_config({ ...common, type: "s3", keyid: "", bucket: "" });
        break;
      case "gcs":
        set_new_config({ ...common, type: "gcs", bucket: "" });
        break;
      case "sshfs":
        set_new_config({
          ...common,
          type: "sshfs",
          user: "",
          host: "",
          path: "",
        });
        break;
      default:
        unreachable(type);
    }
    set_form_readonly(READONLY_DEFAULT);
  }

  function render_instructions() {
    if (!needs_restart) return null;

    return (
      <Alert
        type={"warning"}
        message={
          <Typography.Text strong>
            Restart your project for these changes to take effect.
          </Typography.Text>
        }
      />
    );
  }

  // retrieve all datastore configurations – post-processing in reload()
  async function get() {
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: null },
      },
    };
    return (await webapp_client.query({ query })).query.project_datastore;
  }

  // send the new data to the database
  async function set(config: any) {
    if (config.name == "delete") {
      set_error(`Sorry, you can't name the datastore "delete"`);
      return;
    }
    // we have to check if the name isn't ""
    const name_change = new_config?.name && new_config.name != config.name;
    // if we edit a datastore's name, we have to pick the secret from the one that's going to be deleted
    if (new_config != null && name_change) {
      config.__old_name = new_config.name;
    }
    // the hub will process the config, we just have to do this here to send it
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: config },
      },
    };
    const res = await webapp_client.query({ query });
    if (res.event == "error") {
      set_error(`Problem saving information: ${res.error}`);
    } else {
      set_needs_restart(true);
      set_new_config(null); // hide form
      reload(); // refresh what we just saved ...
    }

    // if we *edit* an entry, get rid of the old one
    if (new_config != null && name_change) {
      del(new_config.name);
    }
  }

  // delete one particular configuration with the given name
  async function del(name: string) {
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: { delete: name } },
      },
    };
    const res = await webapp_client.query({ query });
    if (res.event == "error") {
      set_error(`Problem deleting: ${res.error}`);
    } else {
      set_needs_restart(true);
      reload(); // refresh what we just modified ...
    }
  }

  async function reload() {
    try {
      set_loading(true);
      set_error("");
      const raw = await get();
      if (!is_mounted_ref.current) return;
      if (raw.type === "error") {
        // we encountered an error
        set_error(raw.error);
      }
      if (raw.addons?.datastore == null) {
        set_configs([]);
      } else {
        set_configs(raw2configs(raw.addons.datastore));
      }
    } catch (err) {
      if (err) set_error(err);
    } finally {
      set_loading(false);
    }
  }

  // reload once after mounting
  React.useEffect(() => {
    reload();
  }, []);

  // when we change the new_config data, we also want to reflect that in the corresponding form values
  React.useEffect(() => {
    if (new_config == null) return;
    switch (new_config.type) {
      case "s3":
        form_s3.setFieldsValue(new_config);
        break;
      case "gcs":
        form_gcs.setFieldsValue(new_config);
        break;
      case "sshfs":
        form_sshfs.setFieldsValue(new_config);
        break;
      default:
        unreachable(new_config);
    }
  }, [new_config]);

  function edit(record) {
    if (record == null) return;
    const conf: Config = Object.assign({}, record);
    conf.secret = DUMMY_SECRET;
    set_new_config(conf);
    set_form_readonly(conf.readonly ?? READONLY_DEFAULT);
  }

  function render_list_ro_title() {
    return (
      <Tip
        title="Read-only"
        tip="An open lock indicates the datastore will be mounted with read/write rights, while a closed lock means it will be mounted with read-only options."
      >
        <Icon name="edit" />
      </Tip>
    );
  }

  async function confirm_del(name) {
    try {
      confirm_del(name);
    } catch (err) {
      set_error(err);
    }
  }

  function render_action_buttons(_, record) {
    return (
      <AntdSpace>
        <Tooltip title={`Modify ${record.name}'s configuration.`}>
          <Button onClick={() => edit(record)} icon={<EditOutlined />}></Button>
        </Tooltip>
        <Popconfirm<commit_msg>webapp/datastore: fix delete<commit_after>/*
 *  This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
 *  License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
 */

/*
Datastore (kucalc only!)
*/

import { React, useState, useIsMountedRef } from "../../app-framework";
import { webapp_client } from "../../webapp-client";
import { useProjectState } from "../page/project-state-hook";
import { useProjectHasInternetAccess } from "./has-internet-access-hook";
import { ReloadOutlined, DeleteOutlined } from "@ant-design/icons";
import { PlusCircleOutlined, EditOutlined } from "@ant-design/icons";
import { Button, Table, Typography, Form, Input, Checkbox } from "antd";
import { Space as AntdSpace, Alert, Switch, Popconfirm, Tooltip } from "antd";
import { ErrorDisplay, SettingBox, Space, Icon, Tip, A } from "../../r_misc";
import { unreachable } from "smc-util/misc";
import { DUMMY_SECRET } from "./const";
import { DatastoreConfig as Config } from "./types";

const SECRET_TOOLTIP = `\nSecrets can't be edited. Either keep "${DUMMY_SECRET}" as it is to retain the current value, or enter a new secret to replace the existing one.`;

const DOC = "https://doc.cocalc.com/project-settings.html#datastore";

const RULE_REQUIRED = [
  { required: true, message: "This is a required field." },
];

const RULE_ALPHANUM = [
  RULE_REQUIRED[0],
  {
    pattern: /^[0-9a-z-_.]{1,63}$/,
    message:
      'Must be lowercase alphanumeric and no spaces, i.e. a-z, 0-9, "-", "." and "_" are allowed. Max. 63 characters long.',
  },
];

// convert the configuration from the DB to fields for the table
function raw2configs(raw: { [name: string]: Config }): Config[] {
  const ret: Config[] = [];
  for (const [k, v] of Object.entries(raw)) {
    v.name = k;
    v.key = k; // for antd, to have unique rows
    switch (v.type) {
      case "s3":
        v.about = [`Key ID: ${v.keyid}`, `Bucket: ${v.bucket}`].join("\n");
        break;
      case "gcs":
        v.about = `Bucket: ${v.bucket}`;
        break;
      case "sshfs":
        v.about = [
          `User: ${v.user}`,
          `Host: ${v.host}`,
          `Path: ${v.path ?? `/user/${v.user}`}`,
        ].join("\n");
        break;
      default:
        unreachable(v);
    }
    ret.push(v);
  }
  return ret;
}

const READONLY_DEFAULT = false;

interface Props {
  project_id: string;
}

export const Datastore: React.FC<Props> = React.memo((props: Props) => {
  const { project_id } = props;
  const state = useProjectState(project_id);
  const has_internet = useProjectHasInternetAccess(project_id);
  const is_running = state.get("state") === "running";
  const [needs_restart, set_needs_restart] = useState<boolean>(false);
  const [loading, set_loading] = useState<boolean>(false);
  const [error, set_error] = useState<string>("");
  const [new_config, set_new_config] = useState<Config | null>(null);
  const [show_help, set_show_help] = useState<boolean>(false);
  const editing = new_config != null;
  const is_mounted_ref = useIsMountedRef();
  const [configs, set_configs] = useState<Config[]>([]);
  const [form_readonly, set_form_readonly] = useState<boolean>(
    READONLY_DEFAULT
  );

  const [form_gcs] = Form.useForm();
  const [form_s3] = Form.useForm();
  const [form_sshfs] = Form.useForm();

  React.useEffect(() => {
    // if there is a change to that project running again, we clear the restart warning
    if (is_running && needs_restart) set_needs_restart(false);
  }, [is_running]);

  async function add(type: Config["type"]): Promise<void> {
    const common = { name: "", secret: "" };
    switch (type) {
      case "s3":
        set_new_config({ ...common, type: "s3", keyid: "", bucket: "" });
        break;
      case "gcs":
        set_new_config({ ...common, type: "gcs", bucket: "" });
        break;
      case "sshfs":
        set_new_config({
          ...common,
          type: "sshfs",
          user: "",
          host: "",
          path: "",
        });
        break;
      default:
        unreachable(type);
    }
    set_form_readonly(READONLY_DEFAULT);
  }

  function render_instructions() {
    if (!needs_restart) return null;

    return (
      <Alert
        type={"warning"}
        message={
          <Typography.Text strong>
            Restart your project for these changes to take effect.
          </Typography.Text>
        }
      />
    );
  }

  // retrieve all datastore configurations – post-processing in reload()
  async function get() {
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: null },
      },
    };
    return (await webapp_client.query({ query })).query.project_datastore;
  }

  // send the new data to the database
  async function set(config: any) {
    if (config.name == "delete") {
      set_error(`Sorry, you can't name the datastore "delete"`);
      return;
    }
    // we have to check if the name isn't ""
    const name_change = new_config?.name && new_config.name != config.name;
    // if we edit a datastore's name, we have to pick the secret from the one that's going to be deleted
    if (new_config != null && name_change) {
      config.__old_name = new_config.name;
    }
    // the hub will process the config, we just have to do this here to send it
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: config },
      },
    };
    const res = await webapp_client.query({ query });
    if (res.event == "error") {
      set_error(`Problem saving information: ${res.error}`);
    } else {
      set_needs_restart(true);
      set_new_config(null); // hide form
      if (!name_change) reload(); // refresh what we just saved ...
    }

    // if we *edit* an entry, get rid of the old one
    if (new_config != null && name_change) {
      del(new_config.name);
      reload();
    }
  }

  // delete one particular configuration with the given name
  async function del(name: string) {
    const query = {
      project_datastore: {
        project_id,
        addons: { datastore: { delete: name } },
      },
    };
    const res = await webapp_client.query({ query });
    if (res.event == "error") {
      set_error(`Problem deleting: ${res.error}`);
    } else {
      set_needs_restart(true);
      reload(); // refresh what we just modified ...
    }
  }

  async function reload() {
    try {
      set_loading(true);
      set_error("");
      const raw = await get();
      if (!is_mounted_ref.current) return;
      if (raw.type === "error") {
        // we encountered an error
        set_error(raw.error);
      }
      if (raw.addons?.datastore == null) {
        set_configs([]);
      } else {
        set_configs(raw2configs(raw.addons.datastore));
      }
    } catch (err) {
      if (err) set_error(err);
    } finally {
      set_loading(false);
    }
  }

  // reload once after mounting
  React.useEffect(() => {
    reload();
  }, []);

  // when we change the new_config data, we also want to reflect that in the corresponding form values
  React.useEffect(() => {
    if (new_config == null) return;
    switch (new_config.type) {
      case "s3":
        form_s3.setFieldsValue(new_config);
        break;
      case "gcs":
        form_gcs.setFieldsValue(new_config);
        break;
      case "sshfs":
        form_sshfs.setFieldsValue(new_config);
        break;
      default:
        unreachable(new_config);
    }
  }, [new_config]);

  function edit(record) {
    if (record == null) return;
    const conf: Config = Object.assign({}, record);
    conf.secret = DUMMY_SECRET;
    set_new_config(conf);
    set_form_readonly(conf.readonly ?? READONLY_DEFAULT);
  }

  function render_list_ro_title() {
    return (
      <Tip
        title="Read-only"
        tip="An open lock indicates the datastore will be mounted with read/write rights, while a closed lock means it will be mounted with read-only options."
      >
        <Icon name="edit" />
      </Tip>
    );
  }

  async function confirm_del(name) {
    try {
      del(name);
    } catch (err) {
      set_error(err);
    }
  }

  function render_action_buttons(_, record) {
    return (
      <AntdSpace>
        <Tooltip title={`Modify ${record.name}'s configuration.`}>
          <Button onClick={() => edit(record)} icon={<EditOutlined />}></Button>
        </Tooltip>
        <Popconfirm
{'fraction_non_alphanumeric': 0.14266035814817032, 'fraction_numerical': 0.00275498592561538, 'mean_word_length': 2.615068196579346, 'pattern_counts': {'":': 18, '<': 47, '<?xml version=': 0, '>': 55, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3861691', 'n_tokens_mistral': 5428, 'n_tokens_neox': 5350, 'n_words': 1444}